Compare commits

...

10 Commits

Author SHA1 Message Date
doublex001 e4edbef9e2 chore: 联调审计脚本与 OpenAPI 快照 1 month ago
doublex001 1b1f6e573d refactor(api): 联调地基收尾(auth/双Token/角色/环境配置/品牌资源) 1 month ago
doublex001 86c543c3d7 docs: 归档联调路径校正映射与更新问题清单状态 1 month ago
doublex001 55a9222e77 refactor(api): 按 OpenAPI 校正 89 个接口路径,69 幽灵域退回 mock-only 1 month ago
doublex001 075f7c1f41 feat(api): 加 mock 开关并转岗位域候选读取接口(业务端点连接口) 1 month ago
doublex001 3ef1df2eab feat(auth): 接入后端联调地基(统一请求层+双Token+角色权威化) 1 month ago
doublex001 c16ce63039 feat(auth): 隔离运营商取号适配层,补齐取号超时/网络预检/缓存管理 1 month ago
doublex001 410b1b7386 refactor(theme): 卡片间隔令牌化收尾,borderRadius/padding:8 收敛为 spacing.card 1 month ago
doublex001 aeaa0472ab feat(product): 落地产品待定三项(良好取色统一/去悬空经验档/进度页抽样) 1 month ago
doublex001 b0ed17b17b feat(theme): 收敛 app 框架 padding:16 至 spacing.framework 单一来源 1 month ago
  1. 9
      AGENTS.md
  2. 5
      app.config.js
  3. 36
      app/(enterprise)/enterprise-chat.tsx
  4. 20
      app/(enterprise)/enterprise-overview.tsx
  5. 54
      app/(enterprise)/job-management.tsx
  6. 40
      app/(enterprise)/talent-recommendation.tsx
  7. 40
      app/(tabs)/chat.tsx
  8. 38
      app/(tabs)/growth.tsx
  9. 22
      app/(tabs)/jobs.tsx
  10. 54
      app/(tabs)/profile.tsx
  11. 26
      app/ability.tsx
  12. 30
      app/appeal.tsx
  13. 10
      app/article/[id].tsx
  14. 14
      app/assessment.tsx
  15. 20
      app/audit-report.tsx
  16. 36
      app/candidate-detail.tsx
  17. 24
      app/chat/[id].tsx
  18. 24
      app/enterprise-certification.tsx
  19. 6
      app/favorites.tsx
  20. 16
      app/game.tsx
  21. 34
      app/game/level.tsx
  22. 6
      app/guide.tsx
  23. 6
      app/history.tsx
  24. 28
      app/holland-result.tsx
  25. 24
      app/holland.tsx
  26. 20
      app/holland/question.tsx
  27. 36
      app/hr-evaluation.tsx
  28. 34
      app/interview-score.tsx
  29. 24
      app/interview.tsx
  30. 46
      app/job-evaluations.tsx
  31. 6
      app/job-preference.tsx
  32. 27
      app/job-publish.tsx
  33. 22
      app/jobs/[id].tsx
  34. 20
      app/jobs/compare.tsx
  35. 24
      app/jobs/offer-compare.tsx
  36. 28
      app/jobs/rank.tsx
  37. 24
      app/jobs/weight-settings.tsx
  38. 40
      app/legal.tsx
  39. 16
      app/legal/chat.tsx
  40. 16
      app/legal/checkout.tsx
  41. 10
      app/legal/faq.tsx
  42. 28
      app/legal/service.tsx
  43. 14
      app/legal/template.tsx
  44. 10
      app/legal/templates.tsx
  45. 16
      app/legal/upgrade.tsx
  46. 24
      app/onboarding.tsx
  47. 22
      app/operation-rules.tsx
  48. 32
      app/personal-data-export.tsx
  49. 81
      app/progress.tsx
  50. 28
      app/resume.tsx
  51. 4
      app/resume/basic.tsx
  52. 8
      app/resume/education.tsx
  53. 8
      app/resume/experience.tsx
  54. 26
      app/resume/export.tsx
  55. 6
      app/resume/preview.tsx
  56. 8
      app/resume/project.tsx
  57. 14
      app/settings.tsx
  58. 18
      app/settings/about.tsx
  59. 14
      app/settings/account-cancel.tsx
  60. 12
      app/settings/account.tsx
  61. 20
      app/settings/delete-data.tsx
  62. 12
      app/settings/notification.tsx
  63. 12
      app/settings/password.tsx
  64. 16
      app/settings/privacy.tsx
  65. 18
      app/settings/privacy/[id].tsx
  66. 20
      app/settings/privacy/dnd.tsx
  67. 22
      app/settings/privacy/keywords.tsx
  68. 20
      app/settings/qualifications.tsx
  69. 20
      app/tools/salary-calculator.tsx
  70. 48
      app/tools/skill-cloud.tsx
  71. BIN
      assets/brand-logo.jpg
  72. BIN
      assets/favicon.png
  73. BIN
      assets/icon.png
  74. BIN
      assets/splash-icon.png
  75. 2
      components/onboarding/screens/B1Wechat.tsx
  76. 6
      components/onboarding/screens/P12Company.tsx
  77. 4
      components/onboarding/screens/P17Reference.tsx
  78. 2
      components/onboarding/screens/P1Login.tsx
  79. 24
      components/onboarding/screens/P2PasswordLogin.tsx
  80. 2
      components/onboarding/screens/P2PasswordReset.tsx
  81. 4
      components/onboarding/screens/P2PhoneCode.tsx
  82. 2
      components/onboarding/screens/P2WechatCode.tsx
  83. 2
      components/onboarding/screens/P3Identity.tsx
  84. 2
      components/onboarding/ui/BrandHeader.tsx
  85. 4
      components/onboarding/ui/CountdownButton.tsx
  86. 4
      components/onboarding/ui/Modal.tsx
  87. 8
      components/onboarding/ui/RadioRow.tsx
  88. 2
      components/onboarding/ui/ScreenShell.tsx
  89. 8
      components/resume/EditKit.tsx
  90. 109
      docs/联调路径校正映射.md
  91. 83
      scripts/audit_all_paths.py
  92. 119
      scripts/docx_text.txt
  93. 36
      scripts/extract_docx.py
  94. 156
      scripts/gen_correction_map.py
  95. 76
      scripts/live_validate.py
  96. 1
      scripts/openapi.json
  97. 20
      scripts/openapi_paths.py
  98. 188
      scripts/transform_api.py
  99. 249
      src/__tests__/backend-bridge-remaining.test.ts
  100. 141
      src/__tests__/backend-bridge-write.test.ts

9
AGENTS.md

@ -9,11 +9,14 @@ Read the exact versioned docs at https://docs.expo.dev/versions/v57.0.0/ before
========================================
【核心原则】
你是react native+expo开发工程师
做任务前,先检查问题有没有错误前提、逻辑跳跃和信息缺失。 不要迎合我,要独立判断,区分事实、预测和主观观点。 涉及数字、人物和结论时尽量核实来源。 不同意就直接指出并给出依据、风险和替代解释。 还要主动提醒我忽略的变量、成本和偏差。
亲爱的,你是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,有页面出口就一定要有入口。

5
app.config.js

@ -75,7 +75,10 @@ export default {
],
],
extra: {
apiBaseUrl: process.env.API_BASE_URL || 'https://dev.api.com',
// 业务接口基地址(规范 §1)。默认生产 HTTPS 域名;联调/本地可用环境变量 API_BASE_URL 覆盖。
apiBaseUrl: process.env.API_BASE_URL || 'https://job.youthome.com.cn/api/v1',
// mock 开关:联调期为 false 统一走真实后端;业务函数按接口是否已接通决定是否绕过 mock。
apiUseMock: false,
// EAS 项目 ID:`eas init` 后会自动注入此处(与 updates.url 中的 PROJECT_ID 对应)。
eas: {
projectId: 'REPLACE_WITH_PROJECT_ID',

36
app/(enterprise)/enterprise-chat.tsx

@ -12,7 +12,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useEnterpriseCommunication } from '../../src/hooks';
import { ENTERPRISE_DISPLAY_NAME } from '../../src/constants';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { EnterpriseChatItem, EnterpriseCommunicationTab, PendingTask } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -420,7 +420,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary,
},
body: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
center: {
@ -433,15 +433,15 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 8,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
retryText: { color: colors.white, fontSize: 13, fontWeight: '600' },
pendingCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
pendingTitle: {
@ -457,8 +457,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.pageBg,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
pendingIconWrap: {
width: 40,
@ -483,7 +483,7 @@ const styles = StyleSheet.create({
pendingBadge: {
backgroundColor: colors.actionLight,
borderRadius: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
marginLeft: 8,
},
@ -516,14 +516,14 @@ const styles = StyleSheet.create({
emptyList: {
alignItems: 'center',
gap: 10,
padding: 8,
padding: spacing.card,
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
},
emptyListText: { fontSize: 13, color: colors.textSecondary },
chatListCard: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
marginBottom: 8,
},
@ -569,7 +569,7 @@ const styles = StyleSheet.create({
},
chatLabelTag: {
backgroundColor: colors.compareOrange + '20',
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: 8,
@ -615,8 +615,8 @@ const styles = StyleSheet.create({
quickActionsCard: {
flexDirection: 'row',
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingVertical: spacing.card,
},
quickAction: {
flex: 1,
@ -629,8 +629,8 @@ const styles = StyleSheet.create({
},
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
emptyIconWrap: {
@ -669,8 +669,8 @@ const styles = StyleSheet.create({
dataCard: {
width: '100%',
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'flex-start',
},
dataTitle: {

20
app/(enterprise)/enterprise-overview.tsx

@ -12,7 +12,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useEnterpriseOverview } from '../../src/hooks';
import { LoadingState, ErrorState } from '../../src/components/StateView';
import { useRole } from '../../src/context/RoleContext';
import { colors, headerGradient, semanticColorTokens } from '../../src/theme';
import {colors, headerGradient, semanticColorTokens, spacing} from '../../src/theme';
import type { EnterpriseOverview, EnterpriseQuickAction, EnterpriseStat, JobEvaluationSummary, MenuItem } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -306,7 +306,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
},
headerBtn: {
@ -323,9 +323,9 @@ const styles = StyleSheet.create({
companyCard: {
marginHorizontal: 8,
marginTop: 8,
padding: 8,
padding: spacing.card,
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
},
companyHeader: {
flexDirection: 'row',
@ -389,13 +389,13 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 32,
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardHeader: {
@ -453,7 +453,7 @@ const styles = StyleSheet.create({
quickItem: {
flex: 1,
alignItems: 'center',
paddingVertical: 8,
paddingVertical: spacing.card,
},
quickIcon: {
width: 48,
@ -532,8 +532,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
candidateIconWrap: {

54
app/(enterprise)/job-management.tsx

@ -17,7 +17,7 @@ import { useJobManagement } from '../../src/hooks';
import { JOB_MANAGEMENT_SORTS, buildJobStatusAction, getJobStatusLabel } from '../../src/api';
import { ErrorState, LoadingState } from '../../src/components/StateView';
import { ENTERPRISE_DISPLAY_NAME } from '../../src/constants';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
import { formatCompact, formatDateTime } from '../../src/utils';
import type { JobManagement, JobManagementJob, JobManagementTab } from '../../src/types';
@ -654,7 +654,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary,
},
body: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
center: {
@ -667,7 +667,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 8,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -683,8 +683,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
statIconWrap: {
width: 40,
@ -723,9 +723,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 10,
paddingVertical: 8,
paddingVertical: spacing.card,
},
searchInput: {
flex: 1,
@ -737,9 +737,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 10,
paddingVertical: 8,
paddingVertical: spacing.card,
},
filterBtnText: {
fontSize: 13,
@ -750,9 +750,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 10,
paddingVertical: 8,
paddingVertical: spacing.card,
},
sortBtnText: {
fontSize: 13,
@ -761,8 +761,8 @@ const styles = StyleSheet.create({
},
jobCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
jobHeader: {
@ -814,8 +814,8 @@ const styles = StyleSheet.create({
},
rankTag: {
backgroundColor: colors.primaryLight,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: 2,
marginLeft: 8,
},
@ -833,7 +833,7 @@ const styles = StyleSheet.create({
jobTag: {
backgroundColor: colors.pageBg,
borderRadius: 6,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 3,
},
jobTagText: {
@ -889,8 +889,8 @@ const styles = StyleSheet.create({
},
statusTag: {
backgroundColor: colors.pageBg,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
statusTagText: {
@ -903,7 +903,7 @@ const styles = StyleSheet.create({
filterBadge: {
minWidth: 16,
height: 16,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primaryBtn,
alignItems: 'center',
justifyContent: 'center',
@ -929,7 +929,7 @@ const styles = StyleSheet.create({
emptyResultBtn: {
marginTop: 4,
paddingHorizontal: 20,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryLight,
},
@ -956,7 +956,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
@ -967,10 +967,10 @@ const styles = StyleSheet.create({
color: colors.textPrimary,
},
sheetBody: {
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
},
sheetFooter: {
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingTop: 12,
},
sheetActions: {
@ -1056,8 +1056,8 @@ const styles = StyleSheet.create({
},
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
emptyIconWrap: {
@ -1096,8 +1096,8 @@ const styles = StyleSheet.create({
benefitCard: {
width: '100%',
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'flex-start',
},
benefitTitle: {

40
app/(enterprise)/talent-recommendation.tsx

@ -13,7 +13,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTalentRecommendation } from '../../src/hooks';
import { ENTERPRISE_DISPLAY_NAME } from '../../src/constants';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
import type { TalentCandidate, TalentRecommendationTab } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -413,7 +413,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: 8,
backgroundColor: colors.white,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingBottom: 10,
},
searchInput: {
@ -421,21 +421,21 @@ const styles = StyleSheet.create({
backgroundColor: colors.pageBg,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 8,
paddingVertical: spacing.card,
fontSize: 14,
color: colors.textPrimary,
},
emptyList: {
alignItems: 'center',
gap: 10,
padding: 8,
padding: spacing.card,
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
},
emptyListText: { fontSize: 13, color: colors.textSecondary },
emptyListBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
@ -479,7 +479,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primary,
},
body: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
center: {
@ -492,7 +492,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 8,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -538,8 +538,8 @@ const styles = StyleSheet.create({
},
candidateCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
candidateHeader: {
@ -575,7 +575,7 @@ const styles = StyleSheet.create({
matchTag: {
backgroundColor: colors.successLight,
borderRadius: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
matchTagText: {
@ -628,7 +628,7 @@ const styles = StyleSheet.create({
tag: {
backgroundColor: colors.pageBg,
borderRadius: 6,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 3,
},
tagText: {
@ -666,8 +666,8 @@ const styles = StyleSheet.create({
},
poolCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
},
poolTitle: {
@ -712,8 +712,8 @@ const styles = StyleSheet.create({
},
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
emptyIconWrap: {
@ -752,8 +752,8 @@ const styles = StyleSheet.create({
actionCard: {
width: '100%',
backgroundColor: colors.actionLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
alignItems: 'flex-start',
},
@ -790,8 +790,8 @@ const styles = StyleSheet.create({
dataCard: {
width: '100%',
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'flex-start',
},
dataTitle: {

40
app/(tabs)/chat.tsx

@ -15,7 +15,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useMessageData } from '../../src/hooks';
import type { Conversation, Interview, Notification } from '../../src/types';
import { colors, headerGradient } from '../../src/theme';
import {colors, headerGradient, spacing} from '../../src/theme';
/** Tab 键 */
type TabKey = 'message' | 'notification' | 'interview';
@ -315,7 +315,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -331,7 +331,7 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
nav: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 8,
paddingBottom: 12,
},
@ -342,7 +342,7 @@ const styles = StyleSheet.create({
},
tabBar: {
flexDirection: 'row',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
@ -374,15 +374,15 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 16,
paddingBottom: 24,
},
// 匹配横幅
matchBanner: {
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
matchBannerTitle: {
@ -400,8 +400,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
avatarWrap: {
@ -456,7 +456,7 @@ const styles = StyleSheet.create({
matchedTag: {
marginTop: 6,
backgroundColor: colors.primaryLight,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 6,
paddingVertical: 2,
},
@ -470,8 +470,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.actionLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
offerTitle: {
@ -488,8 +488,8 @@ const styles = StyleSheet.create({
// 通知卡片
notiCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
notiTitleRow: {
@ -537,8 +537,8 @@ const styles = StyleSheet.create({
},
interviewCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
interviewHeader: {
@ -565,8 +565,8 @@ const styles = StyleSheet.create({
marginTop: 12,
},
typeTag: {
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: 3,
},
typeTagVideo: {
@ -588,8 +588,8 @@ const styles = StyleSheet.create({
actionBtn: {
backgroundColor: colors.primaryBtn,
borderRadius: 18,
paddingHorizontal: 16,
paddingVertical: 8,
paddingHorizontal: spacing.framework,
paddingVertical: spacing.card,
},
actionText: {
fontSize: 13,

38
app/(tabs)/growth.tsx

@ -14,7 +14,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Circle, Line, Polygon } from 'react-native-svg';
import { useGrowthData } from '../../src/hooks';
import { articleThumbnailColors, colors, headerGradient, jobLevelTextColors } from '../../src/theme';
import {articleThumbnailColors, colors, headerGradient, jobLevelTextColors, spacing} from '../../src/theme';
import { formatThousands, formatViews } from '../../src/utils';
import { JOB_LEVEL_LABEL } from '../../src/constants';
import type { AbilityDimension } from '../../src/types';
@ -328,7 +328,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -341,7 +341,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingBottom: 24,
},
/* ---- 顶部导航 ---- */
@ -368,8 +368,8 @@ const styles = StyleSheet.create({
heroCard: {
flexDirection: 'row',
marginTop: 8,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
heroIcon: {
width: 52,
@ -401,8 +401,8 @@ const styles = StyleSheet.create({
},
heroButton: {
borderRadius: 18,
paddingHorizontal: 16,
paddingVertical: 8,
paddingHorizontal: spacing.framework,
paddingVertical: spacing.card,
backgroundColor: colors.white,
},
heroButtonText: {
@ -419,8 +419,8 @@ const styles = StyleSheet.create({
/* ---- 通用分区标题 ---- */
emptyBox: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
gap: 8,
},
@ -444,8 +444,8 @@ const styles = StyleSheet.create({
/* ---- 四维能力测评卡片 ---- */
abilityCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
abilityTop: {
flexDirection: 'row',
@ -472,9 +472,9 @@ const styles = StyleSheet.create({
},
levelBadge: {
marginLeft: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 3,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primaryLight,
},
levelBadgeText: {
@ -534,8 +534,8 @@ const styles = StyleSheet.create({
},
toolCard: {
flex: 1,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
},
toolTitle: {
@ -558,8 +558,8 @@ const styles = StyleSheet.create({
articleCard: {
flexDirection: 'row',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
articleThumb: {
@ -590,9 +590,9 @@ const styles = StyleSheet.create({
fontSize: 11,
color: colors.primaryText,
backgroundColor: colors.primaryLight,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
},
articleViews: {

22
app/(tabs)/jobs.tsx

@ -23,7 +23,7 @@ import {
SALARY_RANGES,
} from '../../src/constants';
import type { Job, SortType } from '../../src/types';
import { colors, headerGradient, jobLevelTextColors } from '../../src/theme';
import { colors, headerGradient, jobLevelTextColors , spacing } from '../../src/theme';
/** 筛选项(下拉占位) */
const FILTERS = ['全部行业', '薪资', '经验', '更多筛选'];
@ -416,7 +416,7 @@ const styles = StyleSheet.create({
nav: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 4,
paddingBottom: 8,
},
@ -462,7 +462,7 @@ const styles = StyleSheet.create({
marginTop: 12,
},
filterContent: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
gap: 12,
},
filterChip: {
@ -484,7 +484,7 @@ const styles = StyleSheet.create({
marginLeft: 4,
minWidth: 16,
height: 16,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 4,
alignItems: 'center',
justifyContent: 'center',
@ -500,7 +500,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
marginTop: 16,
marginBottom: 12,
},
@ -530,7 +530,7 @@ const styles = StyleSheet.create({
compareBtn: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: colors.primaryLight,
@ -562,7 +562,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -575,13 +575,13 @@ const styles = StyleSheet.create({
flex: 1,
},
listContent: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingBottom: 24,
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardPressed: {
@ -695,7 +695,7 @@ const styles = StyleSheet.create({
},
optionChip: {
paddingHorizontal: 14,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.pageBg,
borderWidth: 1,

54
app/(tabs)/profile.tsx

@ -15,18 +15,18 @@ import { UNSET_LABEL } from '../../src/constants';
import { Avatar } from '../../components/onboarding/ui/Avatar';
import { isAvatarPreset } from '../../components/onboarding/assets';
import { useRole } from '../../src/context/RoleContext';
import { colors, headerGradientSolid } from '../../src/theme';
import { colors, headerGradientSolid, jobLevelTextColors , spacing } from '../../src/theme';
import type { Badge, DimensionScore, EnterpriseEntry, JobPreference, MenuItem, UserStats } from '../../src/types';
/* ===================== 展示层映射(全部使用主题令牌) ===================== */
/** 维度等级 → 展示配置(优秀=品牌蓝,良好=灰色) */
/** 维度等级 → 展示配置(优秀/良好 均取品牌蓝,与 jobLevelTextColors 对齐,消除两处取色分歧) */
const DIMENSION_LEVEL: Record<
DimensionScore['level'],
{ label: string; bar: string; bg: string; text: string }
> = {
excellent: { label: '优秀', bar: colors.primary, bg: colors.primaryLight, text: colors.primaryDark },
good: { label: '良好', bar: colors.textHint, bg: colors.pageBg, text: colors.textSecondary },
good: { label: '良好', bar: colors.textHint, bg: colors.pageBg, text: jobLevelTextColors.good },
};
/** 求职偏好字段 → 展示标签 */
@ -398,7 +398,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -409,7 +409,7 @@ const styles = StyleSheet.create({
},
/* ---- 顶部渐变头部 ---- */
headerGradient: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 8,
paddingBottom: 24,
borderBottomLeftRadius: 24,
@ -466,7 +466,7 @@ const styles = StyleSheet.create({
paddingBottom: 24,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 16,
},
/* ---- 综合评分卡片 ---- */
@ -474,9 +474,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
scoreValue: {
fontSize: 40,
@ -536,9 +536,9 @@ const styles = StyleSheet.create({
/* ---- 八大维度(横向评分条) ---- */
dimCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
dimRow: {
flexDirection: 'row',
@ -570,7 +570,7 @@ const styles = StyleSheet.create({
},
levelTag: {
marginLeft: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
borderRadius: 10,
},
@ -581,9 +581,9 @@ const styles = StyleSheet.create({
/* ---- 求职偏好 ---- */
prefCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
prefRow: {
flexDirection: 'row',
@ -633,14 +633,14 @@ const styles = StyleSheet.create({
marginHorizontal: -8,
},
hScrollContent: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
gap: 8,
},
badgeCard: {
width: 140,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
badgeIcon: {
@ -697,8 +697,8 @@ const styles = StyleSheet.create({
statsCard: {
flexDirection: 'row',
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingVertical: spacing.card,
marginBottom: 8,
},
statItem: {
@ -715,9 +715,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
marginBottom: 8,
},
enterpriseIconWrap: {
@ -735,8 +735,8 @@ const styles = StyleSheet.create({
/* ---- 功能菜单 ---- */
menuCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
},
menuRow: {
flexDirection: 'row',
@ -751,7 +751,7 @@ const styles = StyleSheet.create({
menuIconWrap: {
width: 32,
height: 32,
borderRadius: 8,
borderRadius: spacing.card,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,

26
app/ability.tsx

@ -11,7 +11,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg';
import { useAbilityAssessment } from '../src/hooks';
import { colors, jobLevelTextColors } from '../src/theme';
import { colors, jobLevelTextColors , spacing } from '../src/theme';
import { JOB_LEVEL_LABEL } from '../src/constants';
import type { AbilityDimension, AbilityTrendPoint, JobLevel } from '../src/types';
@ -277,12 +277,12 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { padding: 8, paddingBottom: 32 },
body: { padding: spacing.card, paddingBottom: 32 },
scoreCard: {
flexDirection: 'row',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -299,8 +299,8 @@ const styles = StyleSheet.create({
section: { marginTop: 16 },
emptyBox: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
gap: 8,
},
@ -308,8 +308,8 @@ const styles = StyleSheet.create({
sectionTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 },
dimCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -344,8 +344,8 @@ const styles = StyleSheet.create({
subItemScore: { width: 50, fontSize: 12, color: colors.textPrimary, textAlign: 'right' },
chartCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -359,8 +359,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -370,6 +370,6 @@ const styles = StyleSheet.create({
improveDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2, lineHeight: 18 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
});

30
app/appeal.tsx

@ -13,7 +13,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAppealForm, useImagePick } from '../src/hooks';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import type { AppealReason, AppealType } from '../src/types';
/** 申诉类型选项 */
@ -461,20 +461,20 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { paddingHorizontal: 8, paddingBottom: 100, paddingTop: 12 },
body: { paddingHorizontal: spacing.card, paddingBottom: 100, paddingTop: 12 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
noticeCard: {
backgroundColor: colors.infoLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.infoBorder,
@ -490,8 +490,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -513,7 +513,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.white,
borderWidth: 1,
@ -526,9 +526,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingVertical: spacing.card,
paddingHorizontal: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -564,7 +564,7 @@ const styles = StyleSheet.create({
evidenceImage: {
width: 72,
height: 72,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.pageBg,
},
evidenceRemove: {
@ -580,8 +580,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -599,7 +599,7 @@ const styles = StyleSheet.create({
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderTopWidth: 1,

10
app/article/[id].tsx

@ -5,7 +5,7 @@ import type { ComponentProps } from 'react';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useGrowthData } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { articleThumbnailColors, colors } from '../../src/theme';
import { articleThumbnailColors, colors , spacing } from '../../src/theme';
import { formatViews } from '../../src/utils';
type IconName = ComponentProps<typeof Ionicons>['name'];
@ -81,7 +81,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -102,7 +102,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
thumb: {
@ -129,9 +129,9 @@ const styles = StyleSheet.create({
fontSize: 11,
color: colors.primaryText,
backgroundColor: colors.primaryLight,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
marginRight: 10,
},

14
app/assessment.tsx

@ -10,7 +10,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useGrowthData } from '../src/hooks';
import { colors, jobLevelTextColors } from '../src/theme';
import { colors, jobLevelTextColors , spacing } from '../src/theme';
import { JOB_LEVEL_LABEL } from '../src/constants';
import type { JobLevel } from '../src/types';
@ -166,14 +166,14 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { padding: 8, paddingBottom: 32 },
body: { padding: spacing.card, paddingBottom: 32 },
abilityCard: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -190,8 +190,8 @@ const styles = StyleSheet.create({
entryCard: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
entryIcon: {
width: 44,
@ -205,6 +205,6 @@ const styles = StyleSheet.create({
entryDesc: { fontSize: 13, color: colors.textSecondary, marginTop: 2 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
});

20
app/audit-report.tsx

@ -4,7 +4,7 @@ import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Line, Path, Rect, Text as SvgText } from 'react-native-svg';
import { useAuditReport } from '../src/hooks';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
import type { AuditKPI, AuditTrendPoint, RiskDistributionItem } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -253,19 +253,19 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
backgroundColor: colors.white,
},
headerBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
scroll: { flex: 1 },
content: { padding: 8 },
content: { padding: spacing.card },
center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
marginTop: 12,
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
@ -274,8 +274,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.success,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
gradeCircle: {
width: 72,
@ -307,8 +307,8 @@ const styles = StyleSheet.create({
kpiCard: {
width: '48%',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
kpiIconWrap: {
@ -326,8 +326,8 @@ const styles = StyleSheet.create({
kpiChange: { fontSize: 12, fontWeight: '600', marginLeft: 3 },
chartCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
chartGap: { height: 12 },
chartHeader: {

36
app/candidate-detail.tsx

@ -11,7 +11,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Line, Path, Polygon, Text as SvgText } from 'react-native-svg';
import { useCandidateDetail } from '../src/hooks';
import { LoadingState, ErrorState } from '../src/components/StateView';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import type { CandidateAbility, CandidateReview } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -364,7 +364,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -385,13 +385,13 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 100,
},
infoCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
infoHeader: {
@ -429,9 +429,9 @@ const styles = StyleSheet.create({
matchBox: {
alignItems: 'center',
backgroundColor: colors.successLight,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
matchValue: {
fontSize: 20,
@ -470,8 +470,8 @@ const styles = StyleSheet.create({
},
scoreCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
flexDirection: 'row',
justifyContent: 'space-between',
@ -502,8 +502,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardTitle: {
@ -594,7 +594,7 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
reviewTag: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: colors.primaryLight,
@ -612,8 +612,8 @@ const styles = StyleSheet.create({
suggestionRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
marginBottom: 8,
@ -639,8 +639,8 @@ const styles = StyleSheet.create({
},
salaryBox: {
marginTop: 8,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.infoLight,
},
salaryLabel: {
@ -665,7 +665,7 @@ const styles = StyleSheet.create({
},
footer: {
flexDirection: 'row',
padding: 16,
padding: spacing.framework,
paddingBottom: 24,
backgroundColor: colors.white,
borderTopWidth: 1,

24
app/chat/[id].tsx

@ -15,7 +15,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useConversation } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { ChatCard, ChatMessage } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -427,7 +427,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 52,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -487,8 +487,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginHorizontal: 8,
marginTop: 8,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.successLight,
},
bannerIconWrap: {
@ -517,7 +517,7 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 8,
},
systemWrap: {
@ -579,8 +579,8 @@ const styles = StyleSheet.create({
cardBase: {
width: 260,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
},
cardHeader: {
@ -608,7 +608,7 @@ const styles = StyleSheet.create({
cardBullet: {
width: 16,
height: 16,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.successLight,
alignItems: 'center',
justifyContent: 'center',
@ -634,8 +634,8 @@ const styles = StyleSheet.create({
cardOffer: {
width: 260,
backgroundColor: colors.actionLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
},
cardOfferHeader: {
@ -674,7 +674,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 6,
paddingVertical: 8,
paddingVertical: spacing.card,
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
@ -737,7 +737,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

24
app/enterprise-certification.tsx

@ -13,7 +13,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useEnterpriseCertification } from '../src/hooks';
import { useAuth } from '../src/context/AuthContext';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
import type { EnterpriseCertificationForm, EnterpriseCertificationStatus } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -250,7 +250,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
backgroundColor: colors.white,
},
headerBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
@ -261,7 +261,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 12,
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
@ -269,8 +269,8 @@ const styles = StyleSheet.create({
topCard: {
margin: 8,
backgroundColor: colors.primary,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
topHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 16 },
shieldWrap: {
@ -290,7 +290,7 @@ const styles = StyleSheet.create({
statusBadge: {
alignSelf: 'flex-start',
marginTop: 6,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
borderRadius: 10,
},
@ -298,8 +298,8 @@ const styles = StyleSheet.create({
formCard: {
marginHorizontal: 8,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
formTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginBottom: 16 },
field: { marginBottom: 16 },
@ -326,8 +326,8 @@ const styles = StyleSheet.create({
successCard: {
marginHorizontal: 8,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
successTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginTop: 16 },
@ -343,8 +343,8 @@ const styles = StyleSheet.create({
pendingCard: {
marginHorizontal: 8,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
pendingTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginTop: 16 },

6
app/favorites.tsx

@ -5,7 +5,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useFavorites } from '../src/hooks';
import { JobCard } from '../src/components/JobCard';
import { LoadingState, ErrorState, EmptyState } from '../src/components/StateView';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
/**
* 我的收藏页(职位)
@ -58,7 +58,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -79,7 +79,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 12,
paddingBottom: 24,
},

16
app/game.tsx

@ -10,7 +10,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useGameData } from '../src/hooks';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
import type { GameLevel, GameLevelStatus, GameProgress } from '../src/types';
/** 关卡状态 → 样式配置 */
@ -180,7 +180,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -201,13 +201,13 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
progressCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
progressHead: {
@ -242,8 +242,8 @@ const styles = StyleSheet.create({
levelCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
borderWidth: 1,
backgroundColor: colors.white,
},
@ -289,7 +289,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

34
app/game/level.tsx

@ -12,7 +12,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useMemo, useState } from 'react';
import { useGameData } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { GameAnswer, GameQuestion } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -262,7 +262,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -283,7 +283,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
levelDesc: {
@ -291,14 +291,14 @@ const styles = StyleSheet.create({
lineHeight: 20,
color: colors.textSecondary,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
questionCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
questionIndex: {
@ -321,9 +321,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: 8,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.white,
@ -368,8 +368,8 @@ const styles = StyleSheet.create({
},
explanation: {
marginTop: 8,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.infoLight,
},
explanationLabel: {
@ -386,8 +386,8 @@ const styles = StyleSheet.create({
resultCard: {
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
resultTitle: {
@ -406,7 +406,7 @@ const styles = StyleSheet.create({
submitBtn: {
marginTop: 8,
height: 48,
borderRadius: 8,
borderRadius: spacing.card,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.primaryBtn,
@ -422,7 +422,7 @@ const styles = StyleSheet.create({
nextBtn: {
marginTop: 8,
height: 48,
borderRadius: 8,
borderRadius: spacing.card,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.successBtn,
@ -445,7 +445,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

6
app/guide.tsx

@ -12,7 +12,7 @@ import {
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors, onboardingGradient } from '../src/theme';
import {colors, onboardingGradient, spacing} from '../src/theme';
import { useAuth } from '../src/context/AuthContext';
import type { AssetName } from '../components/onboarding/assets';
import { ImageSlot } from '../components/onboarding/ui/ImageSlot';
@ -158,7 +158,7 @@ const styles = StyleSheet.create({
topBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 10,
},
backBtn: {
@ -191,7 +191,7 @@ const styles = StyleSheet.create({
},
bottom: {
alignItems: 'center',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingBottom: 28,
},
dots: { flexDirection: 'row', gap: 8, marginBottom: 24 },

6
app/history.tsx

@ -5,7 +5,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useHistory } from '../src/hooks';
import { JobCard } from '../src/components/JobCard';
import { LoadingState, ErrorState, EmptyState } from '../src/components/StateView';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
/**
* 浏览历史页(职位)
@ -58,7 +58,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -79,7 +79,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 12,
paddingBottom: 24,
},

28
app/holland-result.tsx

@ -15,7 +15,7 @@ import Svg, { Circle, Line, Polygon } from 'react-native-svg';
import { useHollandAssessment } from '../src/hooks';
import { buildHollandReportText } from '../src/api';
import { shareText } from '../src/lib/shareFile';
import { colors, headerGradientShort, hollandTypeColors } from '../src/theme';
import { colors, headerGradientShort, hollandTypeColors , spacing } from '../src/theme';
import type { HollandAction, HollandDimension, HollandTypeAnalysis } from '../src/types';
const RADAR_SIZE = 160;
@ -310,10 +310,10 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { padding: 8, paddingBottom: 32 },
body: { padding: spacing.card, paddingBottom: 32 },
heroCard: {
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
@ -327,8 +327,8 @@ const styles = StyleSheet.create({
sectionTitle: { fontSize: 18, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 },
typeCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -356,8 +356,8 @@ const styles = StyleSheet.create({
careerTagText: { fontSize: 12, color: colors.primaryText },
jobCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -368,15 +368,15 @@ const styles = StyleSheet.create({
matchTag: {
backgroundColor: colors.successLight,
borderRadius: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
matchText: { fontSize: 11, color: colors.successText, fontWeight: '600' },
jobTags: { flexDirection: 'row', gap: 8, marginTop: 10 },
tag: {
backgroundColor: colors.pageBg,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
tagText: { fontSize: 11, color: colors.textSecondary },
@ -385,8 +385,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -396,6 +396,6 @@ const styles = StyleSheet.create({
actionDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
});

24
app/holland.tsx

@ -10,7 +10,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useHollandIntro } from '../src/hooks';
import { colors, hollandTypeColors } from '../src/theme';
import {colors, hollandTypeColors, spacing} from '../src/theme';
import { formatThousands } from '../src/utils';
import type { HollandDimensionIntro, HollandIntro } from '../src/types';
@ -173,15 +173,15 @@ const styles = StyleSheet.create({
headerBtn: { width: 50, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
headerRecord: { fontSize: 14, color: colors.primary, fontWeight: '500' },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
heroCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
marginBottom: 8,
},
@ -228,8 +228,8 @@ const styles = StyleSheet.create({
dimItem: {
width: '30.8%',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
dimBadge: {
@ -264,8 +264,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
featureIcon: {
width: 44,
@ -292,8 +292,8 @@ const styles = StyleSheet.create({
},
advantageCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
advantageTitle: {
fontSize: 15,
@ -322,7 +322,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.white,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingTop: 12,
paddingBottom: 24,
},

20
app/holland/question.tsx

@ -11,7 +11,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useHollandAssessment } from '../../src/hooks';
import { colors, hollandTypeColors } from '../../src/theme';
import { colors, hollandTypeColors , spacing } from '../../src/theme';
import type { HollandDimension, HollandOption } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -200,11 +200,11 @@ const styles = StyleSheet.create({
headerCenter: { alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
headerSub: { fontSize: 12, color: colors.textHint, marginTop: 2 },
body: { padding: 8, paddingBottom: 32 },
body: { padding: spacing.card, paddingBottom: 32 },
dimCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -244,8 +244,8 @@ const styles = StyleSheet.create({
},
questionCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -265,9 +265,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingVertical: spacing.card,
paddingHorizontal: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -306,7 +306,7 @@ const styles = StyleSheet.create({
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},

36
app/hr-evaluation.tsx

@ -11,7 +11,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Circle, Line, Path, Text as SvgText } from 'react-native-svg';
import { useHREvaluation } from '../src/hooks';
import { colors, levelLabelTextColors } from '../src/theme';
import {colors, levelLabelTextColors, spacing} from '../src/theme';
import type { EvaluationDimension, EvaluationReview, EvaluationTrendPoint } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -321,12 +321,12 @@ const styles = StyleSheet.create({
backgroundColor: colors.primaryLight,
},
headerAppealText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
body: { paddingHorizontal: 8, paddingBottom: 100, paddingTop: 12 },
body: { paddingHorizontal: spacing.card, paddingBottom: 100, paddingTop: 12 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
@ -336,8 +336,8 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -361,16 +361,16 @@ const styles = StyleSheet.create({
scoreIcon: { alignItems: 'center', justifyContent: 'center' },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
},
chartCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -388,8 +388,8 @@ const styles = StyleSheet.create({
dimScore: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginRight: 8 },
dimTag: {
backgroundColor: colors.pageBg,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: 2,
marginRight: 8,
},
@ -404,8 +404,8 @@ const styles = StyleSheet.create({
sectionHeader: { marginBottom: 10, marginTop: 4 },
emptyBox: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
gap: 8,
},
@ -413,8 +413,8 @@ const styles = StyleSheet.create({
sectionTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
reviewCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -437,8 +437,8 @@ const styles = StyleSheet.create({
reviewTags: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 10 },
reviewTag: {
backgroundColor: colors.primaryLight,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
reviewTagText: { fontSize: 11, color: colors.primaryText },
@ -460,7 +460,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderTopWidth: 1,

34
app/interview-score.tsx

@ -13,7 +13,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Line, Polygon, Text as SvgText } from 'react-native-svg';
import { useInterviewScore } from '../src/hooks';
import { LoadingState, ErrorState } from '../src/components/StateView';
import { colors, semanticColorTokens } from '../src/theme';
import {colors, semanticColorTokens, spacing} from '../src/theme';
import { formatDateTime } from '../src/utils';
import type { HireSuggestion, InterviewScoreData, InterviewScoreDimension, InterviewScoreTag } from '../src/types';
@ -370,7 +370,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -391,13 +391,13 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 100,
},
infoCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
infoRow: {
@ -419,8 +419,8 @@ const styles = StyleSheet.create({
},
scoreCard: {
backgroundColor: colors.primary,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
flexDirection: 'row',
justifyContent: 'space-between',
@ -447,8 +447,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardTitle: {
@ -536,8 +536,8 @@ const styles = StyleSheet.create({
suggestionRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
marginBottom: 8,
@ -566,8 +566,8 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.infoLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
salaryRange: {
fontSize: 24,
@ -586,7 +586,7 @@ const styles = StyleSheet.create({
},
footer: {
flexDirection: 'row',
padding: 16,
padding: spacing.framework,
paddingBottom: 24,
backgroundColor: colors.white,
borderTopWidth: 1,
@ -615,9 +615,9 @@ const styles = StyleSheet.create({
backgroundColor: colors.infoLight,
borderWidth: 1,
borderColor: colors.infoBorder,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
marginBottom: 8,
},
draftBannerBody: { flex: 1 },

24
app/interview.tsx

@ -13,7 +13,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useAIInterview } from '../src/hooks';
import { buildInterviewConfigText } from '../src/api';
import { shareText } from '../src/lib/shareFile';
import { colors, semanticColorTokens } from '../src/theme';
import {colors, semanticColorTokens, spacing} from '../src/theme';
import type {
InterviewAbilityImprovement,
InterviewMode,
@ -377,12 +377,12 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { paddingHorizontal: 8, paddingBottom: 100, paddingTop: 12 },
body: { paddingHorizontal: spacing.card, paddingBottom: 100, paddingTop: 12 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
@ -393,8 +393,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -413,8 +413,8 @@ const styles = StyleSheet.create({
modeDesc: { fontSize: 12, color: colors.textSecondary, marginTop: 2 },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -436,7 +436,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.white,
borderWidth: 1,
@ -446,7 +446,7 @@ const styles = StyleSheet.create({
typeTextActive: { color: colors.white, fontWeight: '600' },
specialTag: {
paddingHorizontal: 14,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.pageBg,
borderWidth: 1,
@ -458,7 +458,7 @@ const styles = StyleSheet.create({
improvementRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingVertical: spacing.card,
},
improvementLabel: { width: 70, fontSize: 13, color: colors.textPrimary },
improvementTrack: {
@ -484,14 +484,14 @@ const styles = StyleSheet.create({
},
recentDate: { width: 60, fontSize: 12, color: colors.textHint },
recentTitle: { flex: 1, fontSize: 14, color: colors.textPrimary },
recentTag: { borderRadius: 10, paddingHorizontal: 8, paddingVertical: 2 },
recentTag: { borderRadius: 10, paddingHorizontal: spacing.card, paddingVertical: 2 },
recentResult: { fontSize: 12, fontWeight: '600' },
footer: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderTopWidth: 1,

46
app/job-evaluations.tsx

@ -15,7 +15,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import Svg, { Line, Path, Polygon, Text as SvgText } from 'react-native-svg';
import { useJobEvaluationEmptyState, useJobEvaluations } from '../src/hooks';
import { LoadingState, ErrorState } from '../src/components/StateView';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import type {
JobEvaluationDimension,
JobEvaluationEmptyState,
@ -523,7 +523,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -544,13 +544,13 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 32,
},
scoreCard: {
backgroundColor: colors.primary,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
scoreMain: {
@ -605,8 +605,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardTitle: {
@ -617,15 +617,15 @@ const styles = StyleSheet.create({
},
chartCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
alignItems: 'center',
},
radarCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
alignItems: 'center',
},
@ -710,7 +710,7 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
reviewTag: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: colors.primaryLight,
@ -750,8 +750,8 @@ const styles = StyleSheet.create({
/* ---- 回复内容块 ---- */
replyBlock: {
marginTop: 8,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.successLight,
},
replyBlockLabel: {
@ -774,8 +774,8 @@ const styles = StyleSheet.create({
},
modalCard: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
modalTitle: {
fontSize: 16,
@ -791,7 +791,7 @@ const styles = StyleSheet.create({
modalInput: {
marginTop: 12,
minHeight: 88,
borderRadius: 8,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 12,
@ -807,7 +807,7 @@ const styles = StyleSheet.create({
marginTop: 16,
},
modalCancel: {
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 10,
},
modalCancelText: {
@ -833,7 +833,7 @@ const styles = StyleSheet.create({
marginBottom: 16,
},
emptyTabContent: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 12,
gap: 8,
},
@ -869,8 +869,8 @@ const styles = StyleSheet.create({
},
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
marginTop: 8,
},
@ -905,8 +905,8 @@ const styles = StyleSheet.create({
tipCard: {
width: '100%',
backgroundColor: colors.actionLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'flex-start',
},
tipHeader: {

6
app/job-preference.tsx

@ -11,7 +11,7 @@ import {
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
import { useAuth } from '../src/context/AuthContext';
import { useJobPreference } from '../src/hooks';
import {
@ -195,12 +195,12 @@ export default function JobPreferenceScreen() {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},

27
app/job-publish.tsx

@ -13,7 +13,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useJobPublish } from '../src/hooks';
import { LoadingState, ErrorState } from '../src/components/StateView';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import { EDUCATION_OPTIONS, WORK_TYPE_LABELS } from '../src/constants';
import type { JobPublishFormData } from '../src/types';
@ -24,19 +24,16 @@ const WORK_TYPE_OPTIONS = WORK_TYPE_LABELS;
* 经验要求选项 —— **刻意不从 constants 的 `EXPERIENCE_OPTIONS` 取**。
*
* 两张表不是同一个事实,名字像而已:
* · constants 的 `EXPERIENCE_OPTIONS` 是**筛选项**,含「不限」(表示不加限制),
* 且不含「1年以内」;
* · 本表是**企业发布岗位时的必填要求**,没有「不限」(必须明确要求),
* 且含「1年以内」—— 与 `JobExperienceRequirement` 的成员逐一对应。
* · constants 的 `EXPERIENCE_OPTIONS` 是**筛选项**,首项为「不限」(表示不加限制);
* · 本表是**企业发布岗位时的必填要求**,没有「不限」(必须明确要求)。
* 强行合并会让发布页出现「不限」这个不该有的选项。
*
* 已知遗留(非本批处理):`JobExperienceRequirement` 的「1年以内」在 mock 岗位数据里
* **一次都没用到**,而筛选项表里也没有它 —— 即这个取值目前既筛不到、也没有样本。
* 属数据/类型层面的取舍,需产品定夺,已记入《项目开发状态.md》4.16。
* 两者的取值档位现已对齐(同为 应届生 / 1-3年 / 3-5年 / 5-10年 / 10年以上),
* 与 `JobExperienceRequirement` 的成员逐一对应。原先发布端多出的「1年以内」属悬空值
* (筛选侧无对应档位、mock 数据也未使用),已按产品口径移除。
*/
const EXPERIENCE_OPTIONS: JobPublishFormData['experience'][] = [
'应届生',
'1年以内',
'1-3年',
'3-5年',
'5-10年',
@ -402,7 +399,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -434,13 +431,13 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 100,
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardTitle: {
@ -483,7 +480,7 @@ const styles = StyleSheet.create({
},
radioTag: {
paddingHorizontal: 14,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
borderWidth: 1,
borderColor: colors.border,
@ -596,7 +593,7 @@ const styles = StyleSheet.create({
},
footer: {
flexDirection: 'row',
padding: 16,
padding: spacing.framework,
paddingBottom: 24,
backgroundColor: colors.white,
borderTopWidth: 1,

22
app/jobs/[id].tsx

@ -16,7 +16,7 @@ import Svg, { Circle, Line, Polygon, Text as SvgText } from 'react-native-svg';
import { useJobDetail } from '../../src/hooks';
import { buildJobShareText } from '../../src/api';
import { shareText } from '../../src/lib/shareFile';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
import { formatThousands } from '../../src/utils';
import type { JobDimensionScore } from '../../src/types';
@ -369,7 +369,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -418,21 +418,21 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingBottom: 24,
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
/* ---- 空态 ---- */
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingVertical: spacing.card,
paddingHorizontal: spacing.card,
marginBottom: 8,
alignItems: 'center',
gap: 8,
@ -465,7 +465,7 @@ const styles = StyleSheet.create({
},
statusTag: {
backgroundColor: colors.successLight,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 3,
borderRadius: 10,
},
@ -664,7 +664,7 @@ const styles = StyleSheet.create({
commentBar: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 10,
backgroundColor: colors.white,
borderTopWidth: 1,
@ -675,7 +675,7 @@ const styles = StyleSheet.create({
height: 40,
borderRadius: 20,
backgroundColor: colors.pageBg,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
fontSize: 14,
color: colors.textPrimary,
},

20
app/jobs/compare.tsx

@ -7,7 +7,7 @@ import { useRouter } from 'expo-router';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useCompareData } from '../../src/hooks';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
function Header({ onBack }: { onBack: () => void }) {
return (
@ -185,7 +185,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.surface,
borderBottomWidth: StyleSheet.hairlineWidth,
@ -193,7 +193,7 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 32, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '500', color: colors.textPrimary },
content: { padding: 8, paddingBottom: 32 },
content: { padding: spacing.card, paddingBottom: 32 },
stateWrap: {
flex: 1,
alignItems: 'center',
@ -204,8 +204,8 @@ const styles = StyleSheet.create({
stateText: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 8,
paddingVertical: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.primaryLight,
},
retryText: { fontSize: 14, color: colors.primaryText },
@ -231,8 +231,8 @@ const styles = StyleSheet.create({
jobRow: { flexDirection: 'row', gap: 8 },
jobCard: {
flex: 1,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.surface,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.borderLight,
@ -252,11 +252,11 @@ const styles = StyleSheet.create({
},
sectionTitle: { fontSize: 15, fontWeight: '500', color: colors.textPrimary },
card: {
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.surface,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.borderLight,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
},
scoreRow: {
paddingVertical: 12,
@ -296,7 +296,7 @@ const styles = StyleSheet.create({
benefitJob: { fontSize: 13, color: colors.textPrimary, marginBottom: 8 },
benefitTags: { flexDirection: 'row', flexWrap: 'wrap', gap: 6 },
benefitTag: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 3,
borderRadius: 4,
backgroundColor: colors.primaryLight,

24
app/jobs/offer-compare.tsx

@ -15,7 +15,7 @@ import { useOfferCompareData } from '../../src/hooks';
import { buildOfferCompareText } from '../../src/api';
import { shareText } from '../../src/lib/shareFile';
import { EmptyState } from '../../src/components/StateView';
import { colors, headerGradientSolid } from '../../src/theme';
import { colors, headerGradientSolid , spacing } from '../../src/theme';
import type { OfferBenefitGroup, OfferItem, OfferScore, WorkLifeBalanceRow } from '../../src/types';
/* ===================== 子组件 ===================== */
@ -335,7 +335,7 @@ const styles = StyleSheet.create({
height: 48,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
backgroundColor: colors.pageBg,
},
headerBtn: {
@ -368,7 +368,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -383,7 +383,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingTop: 8,
paddingBottom: 12,
},
@ -398,8 +398,8 @@ const styles = StyleSheet.create({
},
offerCard: {
width: 150,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -447,8 +447,8 @@ const styles = StyleSheet.create({
/* ---- 通用卡片与分区标题 ---- */
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
},
sectionHeader: {
@ -536,9 +536,9 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
paddingVertical: spacing.card,
marginHorizontal: 2,
borderRadius: 8,
borderRadius: spacing.card,
},
tableHeaderText: {
fontSize: 13,
@ -576,8 +576,8 @@ const styles = StyleSheet.create({
/* ---- AI 建议 ---- */
aiCard: {
marginTop: 8,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
aiHeader: {
flexDirection: 'row',

28
app/jobs/rank.tsx

@ -14,7 +14,7 @@ import Svg, { Circle, Rect } from 'react-native-svg';
import { useRankData } from '../../src/hooks';
import { buildRankReportText } from '../../src/api';
import { shareText } from '../../src/lib/shareFile';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
import { formatThousands } from '../../src/utils';
import type { DimensionPercentile, RankData, RankEmptyState, TopCandidate } from '../../src/types';
@ -394,7 +394,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 4,
},
headerBtn: {
@ -436,7 +436,7 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingBottom: 40,
},
bottomSpacer: {
@ -460,8 +460,8 @@ const styles = StyleSheet.create({
/* ---- 综合排名卡片 ---- */
rankCard: {
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
borderColor: colors.primaryBorder,
},
@ -494,7 +494,7 @@ const styles = StyleSheet.create({
},
levelBadge: {
backgroundColor: colors.successLight,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
borderRadius: 4,
marginLeft: 8,
@ -523,8 +523,8 @@ const styles = StyleSheet.create({
/* ---- 分区 ---- */
section: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
},
sectionTitle: {
@ -622,14 +622,14 @@ const styles = StyleSheet.create({
},
/* ---- 空状态 ---- */
emptyContent: {
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingTop: 24,
paddingBottom: 40,
},
emptyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
},
emptyIconWrap: {
@ -667,8 +667,8 @@ const styles = StyleSheet.create({
},
progressCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
},
progressTitle: {
@ -754,7 +754,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: colors.primaryLight,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 14,
paddingVertical: 12,
marginTop: 8,

24
app/jobs/weight-settings.tsx

@ -7,7 +7,7 @@ import { useRouter } from 'expo-router';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useWeightSettings } from '../../src/hooks';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
const MIN_WEIGHT = 0;
const MAX_WEIGHT = 100;
@ -175,7 +175,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.surface,
borderBottomWidth: StyleSheet.hairlineWidth,
@ -183,13 +183,13 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 32, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '500', color: colors.textPrimary },
content: { padding: 8, paddingBottom: 24 },
content: { padding: spacing.card, paddingBottom: 24 },
stateWrap: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 },
stateText: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 8,
paddingVertical: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.primaryLight,
},
retryText: { fontSize: 14, color: colors.primaryText },
@ -197,8 +197,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.primaryLight,
},
summaryCardWarn: { backgroundColor: colors.dangerLight },
@ -218,8 +218,8 @@ const styles = StyleSheet.create({
templateRow: { gap: 8, paddingRight: 16 },
templateCard: {
width: 140,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.surface,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.borderLight,
@ -227,11 +227,11 @@ const styles = StyleSheet.create({
templateName: { fontSize: 14, fontWeight: '500', color: colors.textPrimary, marginBottom: 4 },
templateDesc: { fontSize: 11, color: colors.textSecondary },
dimensionCard: {
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.surface,
borderWidth: StyleSheet.hairlineWidth,
borderColor: colors.borderLight,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
},
dimRow: {
paddingVertical: 14,
@ -268,7 +268,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primaryLight,
},
footer: {
padding: 16,
padding: spacing.framework,
backgroundColor: colors.surface,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: colors.borderLight,

40
app/legal.tsx

@ -11,7 +11,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData, useLegalMembership } from '../src/hooks';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
import type { LegalFAQ, LegalPlan, LegalService, LegalTemplate } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -336,7 +336,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -357,15 +357,15 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
brandCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.primaryBorder,
@ -424,8 +424,8 @@ const styles = StyleSheet.create({
},
emptyHint: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
gap: 8,
},
@ -436,8 +436,8 @@ const styles = StyleSheet.create({
serviceCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
},
serviceCardFree: {
@ -494,8 +494,8 @@ const styles = StyleSheet.create({
templateCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
},
templateBody: {
@ -514,8 +514,8 @@ const styles = StyleSheet.create({
},
faqCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
},
faqItem: {
paddingVertical: 12,
@ -546,8 +546,8 @@ const styles = StyleSheet.create({
},
planCard: {
flex: 1,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -571,9 +571,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: 8,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.actionLight,
borderWidth: 1,
borderColor: colors.action,
@ -646,7 +646,7 @@ const styles = StyleSheet.create({
gap: 8,
marginTop: 20,
height: 48,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primaryBtn,
},
consultText: {
@ -667,7 +667,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

16
app/legal/chat.tsx

@ -14,7 +14,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { LegalMessage } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -151,7 +151,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -172,7 +172,7 @@ const styles = StyleSheet.create({
flex: 1,
},
scrollContent: {
padding: 8,
padding: spacing.card,
paddingBottom: 16,
},
tipCard: {
@ -180,8 +180,8 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: 8,
backgroundColor: colors.infoLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
tipText: {
@ -243,7 +243,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 10,
backgroundColor: colors.white,
borderTopWidth: 1,
@ -257,7 +257,7 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderColor: colors.border,
paddingHorizontal: 14,
paddingVertical: 8,
paddingVertical: spacing.card,
fontSize: 14,
color: colors.textPrimary,
backgroundColor: colors.pageBg,
@ -286,7 +286,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

16
app/legal/checkout.tsx

@ -5,7 +5,7 @@ import { ActivityIndicator, Alert, Pressable, ScrollView, StyleSheet, Text, View
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData, useLegalMembership } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { LegalPaymentChannel } from '../../src/types';
/* ===================== 付款方式 ===================== */
@ -198,7 +198,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -219,12 +219,12 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
summaryCard: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -280,8 +280,8 @@ const styles = StyleSheet.create({
channelRow: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -353,7 +353,7 @@ const styles = StyleSheet.create({
},
payBtn: {
height: 48,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primaryBtn,
alignItems: 'center',
justifyContent: 'center',

10
app/legal/faq.tsx

@ -5,7 +5,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useState } from 'react';
import { useLegalData } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { LegalFAQ } from '../../src/types';
/* ===================== 顶部导航 ===================== */
@ -82,7 +82,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -103,13 +103,13 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
faqCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
},
faqItem: {
paddingVertical: 12,

28
app/legal/service.tsx

@ -14,7 +14,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData, useLegalMembership, useLegalServiceRequests } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 顶部导航 ===================== */
@ -233,7 +233,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -254,12 +254,12 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
card: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -310,7 +310,7 @@ const styles = StyleSheet.create({
input: {
minHeight: 120,
padding: 12,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -327,7 +327,7 @@ const styles = StyleSheet.create({
submitBtn: {
marginTop: 16,
height: 48,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primaryBtn,
alignItems: 'center',
justifyContent: 'center',
@ -347,8 +347,8 @@ const styles = StyleSheet.create({
marginTop: 16,
},
emptyCard: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
alignItems: 'center',
},
@ -357,8 +357,8 @@ const styles = StyleSheet.create({
color: colors.textSecondary,
},
errorCard: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.dangerLight,
borderWidth: 1,
borderColor: colors.dangerBorder,
@ -378,8 +378,8 @@ const styles = StyleSheet.create({
gap: 8,
},
requestCard: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -397,7 +397,7 @@ const styles = StyleSheet.create({
marginRight: 8,
},
statusTag: {
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
borderRadius: 4,
backgroundColor: colors.primaryLight,

14
app/legal/template.tsx

@ -4,7 +4,7 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 顶部导航 ===================== */
@ -78,7 +78,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -99,14 +99,14 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
headCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
marginBottom: 8,
},
@ -125,8 +125,8 @@ const styles = StyleSheet.create({
color: colors.textSecondary,
},
contentCard: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.borderLight,

10
app/legal/templates.tsx

@ -4,7 +4,7 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { LegalTemplate } from '../../src/types';
/* ===================== 顶部导航 ===================== */
@ -81,7 +81,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -102,7 +102,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
templateGrid: {
@ -111,8 +111,8 @@ const styles = StyleSheet.create({
templateCard: {
flexDirection: 'row',
alignItems: 'center',
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
},
templateBody: {

16
app/legal/upgrade.tsx

@ -5,7 +5,7 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLegalData, useLegalMembership } from '../../src/hooks';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { LegalPlan } from '../../src/types';
/* ===================== 顶部导航 ===================== */
@ -126,7 +126,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -147,7 +147,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
tip: {
@ -159,8 +159,8 @@ const styles = StyleSheet.create({
gap: 8,
},
planCard: {
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.border,
@ -184,9 +184,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: 8,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.actionLight,
borderWidth: 1,
borderColor: colors.action,

24
app/onboarding.tsx

@ -18,7 +18,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { buildChecklistText } from '../src/api';
import { useOnboardingChecklist } from '../src/hooks';
import { shareText } from '../src/lib/shareFile';
import { colors, headerGradientShort } from '../src/theme';
import {colors, headerGradientShort, spacing} from '../src/theme';
import type { ChecklistGroup, OnboardingChecklist, OnboardingTip } from '../src/types';
/** 分组图标配色轮转(全部来自主题 token) */
@ -434,17 +434,17 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { paddingHorizontal: 8, paddingBottom: 100, paddingTop: 12 },
body: { paddingHorizontal: spacing.card, paddingBottom: 100, paddingTop: 12 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
countdownCard: { borderRadius: 8, padding: 8, marginBottom: 8 },
countdownCard: { borderRadius: spacing.card, padding: spacing.card, marginBottom: 8 },
countdownTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
countdownLabel: { fontSize: 13, color: colors.textInverse, opacity: 0.9 },
countdownValueRow: { flexDirection: 'row', alignItems: 'baseline', marginTop: 6 },
@ -477,7 +477,7 @@ const styles = StyleSheet.create({
countdownFill: { height: 8, borderRadius: 4, backgroundColor: colors.white },
groupCard: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -510,15 +510,15 @@ const styles = StyleSheet.create({
itemTitleDone: { textDecorationLine: 'line-through', color: colors.textHint },
requiredTag: {
backgroundColor: colors.dangerLight,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 6,
paddingVertical: 2,
},
requiredText: { fontSize: 10, color: colors.dangerText, fontWeight: '600' },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -543,7 +543,7 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
flexDirection: 'row',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderTopWidth: 1,
@ -577,7 +577,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
paddingHorizontal: 32,
},
modalCard: { backgroundColor: colors.white, borderRadius: 8, padding: 8 },
modalCard: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card },
modalTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
modalInput: {
marginTop: 14,
@ -590,13 +590,13 @@ const styles = StyleSheet.create({
color: colors.textPrimary,
},
modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 18 },
modalCancel: { paddingHorizontal: 16, paddingVertical: 8 },
modalCancel: { paddingHorizontal: spacing.framework, paddingVertical: spacing.card },
modalCancelText: { fontSize: 14, color: colors.textSecondary },
modalConfirm: {
backgroundColor: colors.primaryBtn,
borderRadius: 18,
paddingHorizontal: 20,
paddingVertical: 8,
paddingVertical: spacing.card,
},
modalConfirmText: { fontSize: 14, fontWeight: '700', color: colors.white },
});

22
app/operation-rules.tsx

@ -13,7 +13,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useOperationRules } from '../src/hooks';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import type { MatchMode, OperationRules } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -326,7 +326,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
backgroundColor: colors.white,
},
headerBtn: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
@ -337,7 +337,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 12,
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},
@ -354,9 +354,9 @@ const styles = StyleSheet.create({
card: {
marginHorizontal: 8,
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
modeRow: {
flexDirection: 'row',
@ -375,7 +375,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.successLight,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
borderRadius: spacing.card,
},
recommendedText: { fontSize: 10, fontWeight: '600', color: colors.successText },
modeDesc: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
@ -414,7 +414,7 @@ const styles = StyleSheet.create({
marginLeft: -8,
width: 16,
height: 16,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.white,
borderWidth: 2,
borderColor: colors.primary,
@ -439,7 +439,7 @@ const styles = StyleSheet.create({
height: 32,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
borderRadius: spacing.card,
textAlign: 'center',
fontSize: 14,
color: colors.textPrimary,
@ -475,8 +475,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.textPrimary,
paddingHorizontal: 16,
paddingVertical: 8,
paddingHorizontal: spacing.framework,
paddingVertical: spacing.card,
borderRadius: 18,
},
savingText: { marginLeft: 8, fontSize: 13, color: colors.textInverse },

32
app/personal-data-export.tsx

@ -12,7 +12,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { usePersonalDataExport } from '../src/hooks';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import type { DataExportCategory, DataExportFormat, PersonalDataExport } from '../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -387,7 +387,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -405,7 +405,7 @@ const styles = StyleSheet.create({
color: colors.textPrimary,
},
body: {
padding: 8,
padding: spacing.card,
paddingBottom: 100,
},
center: {
@ -418,7 +418,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 8,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -426,8 +426,8 @@ const styles = StyleSheet.create({
introCard: {
flexDirection: 'row',
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
introIconWrap: {
@ -470,7 +470,7 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
},
rowBorder: {
@ -480,7 +480,7 @@ const styles = StyleSheet.create({
categoryRow: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 14,
},
checkbox: {
@ -519,8 +519,8 @@ const styles = StyleSheet.create({
formatCard: {
flex: 1,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -563,7 +563,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 14,
},
timeLabel: {
@ -594,8 +594,8 @@ const styles = StyleSheet.create({
},
previewCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
previewTitle: {
fontSize: 14,
@ -624,7 +624,7 @@ const styles = StyleSheet.create({
recentRow: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
},
recentIconWrap: {
@ -659,7 +659,7 @@ const styles = StyleSheet.create({
statusTag: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
borderRadius: spacing.card,
},
statusCompleted: {
backgroundColor: colors.successLight,
@ -700,7 +700,7 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
backgroundColor: colors.white,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingTop: 12,
paddingBottom: 24,
borderTopWidth: 1,

81
app/progress.tsx

@ -1,7 +1,7 @@
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
ActivityIndicator,
Pressable,
@ -12,7 +12,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useJobProgress } from '../src/hooks';
import { colors, headerGradientShort } from '../src/theme';
import { colors, headerGradientShort , spacing } from '../src/theme';
import type { JobProgressCompany, JobProgressStage } from '../src/types';
const TABS: { key: 'all' | JobProgressStage; label: string }[] = [
@ -231,13 +231,28 @@ function ErrorState({ onRetry }: { onRetry: () => void }) {
export default function ProgressScreen() {
const router = useRouter();
const { progress, loading, error, refresh } = useJobProgress();
// 公司列表首屏抽样条数:列表过长时只列前 N 家,并提供「查看全部」入口(移动端长列表标准做法)
const PREVIEW_COUNT = 5;
const [activeTab, setActiveTab] = useState<'all' | JobProgressStage>('all');
const [showAll, setShowAll] = useState(false);
const totalCompanies = progress?.companies.length ?? 0;
const companies = useMemo(() => {
if (!progress) return [];
if (activeTab === 'all') return progress.companies;
return progress.companies.filter((c) => c.stage === activeTab);
}, [progress, activeTab]);
const list =
activeTab === 'all'
? progress.companies
: progress.companies.filter((c) => c.stage === activeTab);
// 「全部」页首屏只抽样前 N 家;切到具体阶段或展开后展示全量
if (activeTab === 'all' && !showAll) return list.slice(0, PREVIEW_COUNT);
return list;
}, [progress, activeTab, showAll]);
// 切换 Tab 时重置「展开全部」,回到抽样视图
useEffect(() => {
setShowAll(false);
}, [activeTab]);
// 标题文字跟随当前 Tab,与下方列表同源(见渲染处注释)
const activeLabel = TABS.find((tab) => tab.key === activeTab)?.label ?? '全部';
@ -286,7 +301,18 @@ export default function ProgressScreen() {
<Text style={styles.emptyHint}>投递岗位后,进度会在这里按阶段更新</Text>
</View>
) : (
companies.map((company) => <CompanyRow key={company.id} company={company} />)
<>
{companies.map((company) => (
<CompanyRow key={company.id} company={company} />
))}
{/* 全部页且公司数超过抽样条数:提供「查看全部 N 家」入口,展开后渲染完整列表 */}
{activeTab === 'all' && totalCompanies > PREVIEW_COUNT && !showAll && (
<Pressable style={styles.viewAllBtn} onPress={() => setShowAll(true)} hitSlop={4}>
<Text style={styles.viewAllText}>查看全部 {totalCompanies} 家</Text>
<Ionicons name="chevron-forward" size={16} color={colors.primary} />
</Pressable>
)}
</>
)}
{progress.offers.length > 0 && (
@ -318,10 +344,23 @@ const styles = StyleSheet.create({
},
headerBtn: { width: 40, alignItems: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
body: { padding: 8, paddingBottom: 32 },
body: { padding: spacing.card, paddingBottom: 32 },
viewAllBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
paddingVertical: 12,
},
viewAllText: {
fontSize: 14,
fontWeight: '600',
color: colors.primary,
marginRight: 2,
},
statsCard: {
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
statsRow: { flexDirection: 'row', justifyContent: 'space-between' },
@ -332,8 +371,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.actionBorder,
@ -357,8 +396,8 @@ const styles = StyleSheet.create({
tabTextActive: { color: colors.white, fontWeight: '600' },
emptyBox: {
backgroundColor: colors.surface,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
gap: 8,
},
@ -374,8 +413,8 @@ const styles = StyleSheet.create({
sectionTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
companyCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -409,8 +448,8 @@ const styles = StyleSheet.create({
nextAction: { fontSize: 12, color: colors.textSecondary },
offerCard: {
backgroundColor: colors.successLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.successBorder,
@ -420,7 +459,7 @@ const styles = StyleSheet.create({
offerTag: {
backgroundColor: colors.success,
borderRadius: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
offerTagText: { fontSize: 11, fontWeight: '700', color: colors.white },
@ -430,8 +469,8 @@ const styles = StyleSheet.create({
offerDeadline: { fontSize: 12, color: colors.actionText, marginLeft: 4 },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -454,6 +493,6 @@ const styles = StyleSheet.create({
barValue: { width: 20, fontSize: 11, color: colors.textSecondary, textAlign: 'right' },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
});

28
app/resume.tsx

@ -15,7 +15,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { useResume } from '../src/hooks';
import { RESUME_EVAL_MAX_LENGTH, UNSET_LABEL } from '../src/constants';
import { colors } from '../src/theme';
import {colors, spacing} from '../src/theme';
import type { Resume as ResumeType, ResumeExperience, ResumeOptimizePoint, ResumeProject } from '../src/types';
import { formatDateTime } from '../src/utils';
@ -886,23 +886,23 @@ const styles = StyleSheet.create({
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
headerActions: { flexDirection: 'row', gap: 4 },
headerActionBtn: { width: 36, alignItems: 'center' },
body: { padding: 8, paddingBottom: 100 },
body: { padding: spacing.card, paddingBottom: 100 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
},
scoreCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -926,7 +926,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.primaryBtn,
borderRadius: 16,
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
},
optimizeBtnDisabled: { backgroundColor: colors.textDisabled },
optimizeBtnText: { fontSize: 14, fontWeight: '700', color: colors.white },
@ -986,7 +986,7 @@ const styles = StyleSheet.create({
alignSelf: 'flex-start',
backgroundColor: colors.successLight,
borderRadius: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
intentionTagText: { fontSize: 12, color: colors.successText, fontWeight: '600' },
@ -1014,9 +1014,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
marginTop: 8,
backgroundColor: colors.infoLight,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
aiTipText: { flex: 1, fontSize: 12, color: colors.infoText, marginLeft: 6 },
aiOptimizeMiniBtn: {
@ -1033,7 +1033,7 @@ const styles = StyleSheet.create({
marginTop: 8,
backgroundColor: colors.successLight,
borderRadius: 10,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
paddingVertical: 2,
},
optimizedText: { fontSize: 12, color: colors.successText, marginLeft: 4 },
@ -1057,7 +1057,7 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
flexDirection: 'row',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderTopWidth: 1,

4
app/resume/basic.tsx

@ -5,7 +5,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { PrimaryButton } from '../../components/onboarding/ui/PrimaryButton';
import { CenterState, EditHeader, Field, FormNote, PageFooter } from '../../components/resume/EditKit';
import { useResume } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import { isValidEmail, isValidPhone } from '../../src/utils';
/**
@ -131,6 +131,6 @@ interface BasicForm {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
formError: { fontSize: 13, color: colors.dangerText, marginBottom: 8 },
});

8
app/resume/education.tsx

@ -12,7 +12,7 @@ import {
PageFooter,
} from '../../components/resume/EditKit';
import { useResume } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { ResumeEducation } from '../../src/types';
/**
@ -250,13 +250,13 @@ const toForm = (edu: ResumeEducation): EduForm => ({
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
row: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,

8
app/resume/experience.tsx

@ -13,7 +13,7 @@ import {
PageFooter,
} from '../../components/resume/EditKit';
import { useResume } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { ResumeExperience } from '../../src/types';
import { splitYearMonth, yearMonthValue } from '../../src/utils';
@ -296,7 +296,7 @@ const toForm = (exp: ResumeExperience): ExpForm => {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
section: { marginBottom: 18 },
label: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
labelTop: { marginTop: 14, marginBottom: 10 },
@ -308,8 +308,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,

26
app/resume/export.tsx

@ -23,7 +23,7 @@ import {
RESUME_TEMPLATES,
} from '../../src/constants';
import { useResume, useResumeExport } from '../../src/hooks';
import { colors, templateAccentColors } from '../../src/theme';
import {colors, templateAccentColors, spacing} from '../../src/theme';
import type { ResumeExportFormat, ResumeExportSettings } from '../../src/types';
const FORMATS: { key: ResumeExportFormat; label: string; icon: keyof typeof Ionicons.glyphMap }[] = [
@ -622,15 +622,15 @@ const styles = StyleSheet.create({
headerBtn: { width: 60, justifyContent: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
headerSave: { fontSize: 14, color: colors.primary, textAlign: 'right' },
body: { padding: 8, paddingBottom: 100 },
body: { padding: spacing.card, paddingBottom: 100 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, gap: 12 },
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: { paddingHorizontal: 18, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
retryBtn: { paddingHorizontal: 18, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
retryText: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
previewCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
marginBottom: 8,
borderWidth: 1,
@ -661,8 +661,8 @@ const styles = StyleSheet.create({
formatCard: {
flex: 1,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.borderLight,
@ -678,8 +678,8 @@ const styles = StyleSheet.create({
templateCard: {
flex: 1,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.borderLight,
@ -688,7 +688,7 @@ const styles = StyleSheet.create({
templateThumb: {
width: '100%',
height: 80,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primary,
padding: 12,
marginBottom: 8,
@ -700,8 +700,8 @@ const styles = StyleSheet.create({
templateNameActive: { color: colors.primaryText, fontWeight: '700' },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -790,7 +790,7 @@ const styles = StyleSheet.create({
right: 0,
bottom: 0,
flexDirection: 'row',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 12,
backgroundColor: colors.white,
borderTopWidth: 1,

6
app/resume/preview.tsx

@ -5,7 +5,7 @@ import { PrimaryButton } from '../../components/onboarding/ui/PrimaryButton';
import { CenterState, EditHeader, PageFooter } from '../../components/resume/EditKit';
import { useResume } from '../../src/hooks';
import { UNSET_LABEL } from '../../src/constants';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { Resume, ResumeEducation, ResumeExperience, ResumeProject } from '../../src/types';
/**
@ -198,10 +198,10 @@ function hasIntention(intention: Resume['jobIntention']): boolean {
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
paper: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
padding: 20,
borderWidth: 1,
borderColor: colors.borderLight,

8
app/resume/project.tsx

@ -12,7 +12,7 @@ import {
PageFooter,
} from '../../components/resume/EditKit';
import { useResume } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { ResumeProject } from '../../src/types';
/**
@ -205,13 +205,13 @@ const toForm = (proj: ResumeProject): ProjForm => ({
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
body: { padding: 8, paddingBottom: 24 },
body: { padding: spacing.card, paddingBottom: 24 },
row: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,

14
app/settings.tsx

@ -2,7 +2,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../src/theme';
import { colors , spacing } from '../src/theme';
import { useAppCache } from '../src/hooks';
import { formatBytes } from '../src/utils';
import { useAuth } from '../src/context/AuthContext';
@ -240,7 +240,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
},
headerBtn: {
width: 36,
@ -261,7 +261,7 @@ const styles = StyleSheet.create({
},
group: {
marginTop: 16,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
},
groupTitle: {
marginBottom: 8,
@ -271,8 +271,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.surface,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
},
row: {
flexDirection: 'row',
@ -287,7 +287,7 @@ const styles = StyleSheet.create({
iconWrap: {
width: 32,
height: 32,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
@ -308,7 +308,7 @@ const styles = StyleSheet.create({
marginTop: 24,
marginHorizontal: 8,
backgroundColor: colors.surface,
borderRadius: 8,
borderRadius: spacing.card,
paddingVertical: 14,
alignItems: 'center',
},

18
app/settings/about.tsx

@ -3,7 +3,7 @@ import { useRouter } from 'expo-router';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAboutInfo } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -124,7 +124,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -145,14 +145,14 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
brandCard: {
alignItems: 'center',
paddingVertical: 8,
paddingVertical: spacing.card,
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
marginBottom: 8,
},
brandLogo: {
@ -176,9 +176,9 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
marginBottom: 8,
},
cardTitle: {
@ -251,7 +251,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

14
app/settings/account-cancel.tsx

@ -11,7 +11,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAccountCancel } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -180,7 +180,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -203,7 +203,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
stateBox: {
@ -222,7 +222,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -235,8 +235,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
warnText: {
@ -256,7 +256,7 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
},
bulletRow: {

12
app/settings/account.tsx

@ -3,7 +3,7 @@ import { useRouter } from 'expo-router';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useAccountSecurity } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -104,7 +104,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -125,7 +125,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
tip: {
@ -137,8 +137,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
},
row: {
flexDirection: 'row',
@ -188,7 +188,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

20
app/settings/delete-data.tsx

@ -12,7 +12,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useDeleteAllData } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -199,7 +199,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -222,7 +222,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
stateBox: {
@ -241,7 +241,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -252,8 +252,8 @@ const styles = StyleSheet.create({
},
introCard: {
backgroundColor: colors.infoLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
introText: {
@ -272,9 +272,9 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
padding: 8,
padding: spacing.card,
},
row: {
flexDirection: 'row',
@ -319,8 +319,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'flex-start',
gap: 8,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
warnText: {

12
app/settings/notification.tsx

@ -3,7 +3,7 @@ import { useRouter } from 'expo-router';
import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Switch, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useNotificationSettings } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -90,7 +90,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -111,7 +111,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
tip: {
@ -123,8 +123,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
},
row: {
flexDirection: 'row',
@ -165,7 +165,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},

12
app/settings/password.tsx

@ -6,7 +6,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useChangePassword } from '../../src/hooks';
import { useAuth } from '../../src/context/AuthContext';
import { PasswordField } from '../../src/components/PasswordField';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -132,7 +132,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -155,7 +155,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
tip: {
@ -167,9 +167,9 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
borderRadius: spacing.card,
paddingHorizontal: spacing.card,
paddingVertical: spacing.card,
},
gap: {
height: 6,

16
app/settings/privacy.tsx

@ -6,7 +6,7 @@ import { usePrivacyCenter } from '../../src/hooks';
import { buildPrivacySummaryText } from '../../src/api';
import { shareText } from '../../src/lib/shareFile';
import { ErrorState, LoadingState } from '../../src/components/StateView';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { PrivacyLevelInfo, PrivacySettingItem, PrivacySection } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -206,7 +206,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -227,12 +227,12 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
levelCard: {
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
levelRow: {
@ -276,7 +276,7 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
},
row: {
@ -321,8 +321,8 @@ const styles = StyleSheet.create({
},
explanationCard: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
explanationTitle: {

18
app/settings/privacy/[id].tsx

@ -11,7 +11,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { usePrivacyDetailSetting } from '../../../src/hooks';
import { colors } from '../../../src/theme';
import { colors , spacing } from '../../../src/theme';
import type { PrivacyDetailOption } from '../../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -159,7 +159,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -182,7 +182,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
stateBox: {
@ -199,7 +199,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -210,8 +210,8 @@ const styles = StyleSheet.create({
},
introCard: {
backgroundColor: colors.infoLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
introText: {
@ -230,7 +230,7 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
},
row: {
@ -266,8 +266,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
gap: 8,
},
noteText: {

20
app/settings/privacy/dnd.tsx

@ -12,7 +12,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useDndSetting } from '../../../src/hooks';
import { colors } from '../../../src/theme';
import { colors , spacing } from '../../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -165,7 +165,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -188,7 +188,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 24,
},
stateBox: {
@ -205,7 +205,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -215,8 +215,8 @@ const styles = StyleSheet.create({
color: colors.white,
},
statCard: {
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
marginBottom: 8,
},
@ -240,7 +240,7 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
overflow: 'hidden',
},
row: {
@ -276,14 +276,14 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: colors.primary,
paddingVertical: 4,
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
},
noteCard: {
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
gap: 8,
},
noteText: {

22
app/settings/privacy/keywords.tsx

@ -14,7 +14,7 @@ import {
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useKeywordBlock } from '../../../src/hooks';
import { colors } from '../../../src/theme';
import {colors, spacing} from '../../../src/theme';
/* ===================== 子组件:顶部导航 ===================== */
@ -185,7 +185,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -208,7 +208,7 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 16,
},
stateBox: {
@ -225,7 +225,7 @@ const styles = StyleSheet.create({
retryBtn: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 18,
backgroundColor: colors.primaryBtn,
},
@ -235,8 +235,8 @@ const styles = StyleSheet.create({
color: colors.white,
},
statCard: {
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
marginBottom: 8,
},
@ -251,8 +251,8 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
},
cardTitle: {
@ -309,8 +309,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'flex-start',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
gap: 8,
},
noteText: {
@ -334,7 +334,7 @@ const styles = StyleSheet.create({
height: 40,
borderRadius: 20,
backgroundColor: colors.pageBg,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
fontSize: 14,
color: colors.textPrimary,
},

20
app/settings/qualifications.tsx

@ -4,7 +4,7 @@ import { Image, Pressable, ScrollView, StyleSheet, Text, View } from 'react-nati
import { SafeAreaView } from 'react-native-safe-area-context';
import { EmptyState, ErrorState, LoadingState } from '../../src/components/StateView';
import { useQualifications } from '../../src/hooks';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
import type { QualificationLicense } from '../../src/types';
/* ===================== 子组件:顶部导航 ===================== */
@ -127,7 +127,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
height: 48,
backgroundColor: colors.white,
borderBottomWidth: 1,
@ -148,16 +148,16 @@ const styles = StyleSheet.create({
flex: 1,
},
content: {
padding: 8,
padding: spacing.card,
paddingBottom: 32,
gap: 8,
},
companyCard: {
backgroundColor: colors.primaryLight,
borderRadius: 8,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.primaryBorder,
padding: 8,
padding: spacing.card,
},
companyLabel: {
fontSize: 12,
@ -179,10 +179,10 @@ const styles = StyleSheet.create({
},
card: {
backgroundColor: colors.surface,
borderRadius: 8,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.border,
padding: 8,
padding: spacing.card,
},
cardHeader: {
flexDirection: 'row',
@ -198,21 +198,21 @@ const styles = StyleSheet.create({
marginTop: 8,
width: '100%',
height: 180,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.pageBg,
},
/* 图片占位:用「提醒卡片底 + 弱警示描边」语义色,一眼看出「这里是待办」 */
imagePlaceholder: {
marginTop: 8,
height: 132,
borderRadius: 8,
borderRadius: spacing.card,
borderWidth: 1,
borderStyle: 'dashed',
borderColor: colors.actionBorder,
backgroundColor: colors.actionLight,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 8,
paddingHorizontal: spacing.card,
gap: 8,
},
placeholderText: {

20
app/tools/salary-calculator.tsx

@ -16,7 +16,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { useSalaryCalculator, useSalaryRecords, useSalaryToolLinks } from '../../src/hooks';
import { clampSocialBase } from '../../src/api';
import { LoadingState, ErrorState, EmptyState } from '../../src/components/StateView';
import { colors, headerGradientShort } from '../../src/theme';
import { colors, headerGradientShort , spacing } from '../../src/theme';
import {
CITIES,
DEFAULT_SOCIAL_PROFILE,
@ -569,11 +569,11 @@ const styles = StyleSheet.create({
headerBtn: { width: 60, justifyContent: 'center' },
headerTitle: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
headerAction: { fontSize: 14, color: colors.primary, textAlign: 'right' },
body: { padding: 8, paddingBottom: 32 },
body: { padding: spacing.card, paddingBottom: 32 },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -594,7 +594,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.pageBg,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 8,
paddingVertical: spacing.card,
},
input: {
minWidth: 80,
@ -607,8 +607,8 @@ const styles = StyleSheet.create({
selectBox: { flexDirection: 'row', alignItems: 'center' },
selectValue: { fontSize: 15, fontWeight: '600', color: colors.textPrimary, marginRight: 4 },
resultCard: {
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
alignItems: 'center',
marginBottom: 8,
},
@ -637,15 +637,15 @@ const styles = StyleSheet.create({
taxItem: {
flex: 1,
backgroundColor: colors.pageBg,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
},
taxItemLabel: { fontSize: 12, color: colors.textSecondary, marginBottom: 6 },
taxItemValue: { fontSize: 18, fontWeight: '800', color: colors.textPrimary },
taxRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
paddingVertical: spacing.card,
},
taxRowLabel: { fontSize: 14, color: colors.textPrimary },
taxRowValue: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },

48
app/tools/skill-cloud.tsx

@ -14,7 +14,7 @@ import {
import { SafeAreaView } from 'react-native-safe-area-context';
import { IndustrySkillWordCloud } from '../../components/IndustrySkillWordCloud';
import { useSkillCloud, useSkillLearningPath } from '../../src/hooks';
import { colors } from '../../src/theme';
import {colors, spacing} from '../../src/theme';
import type { SkillLearningPath, SkillWord, TopSkill } from '../../src/types';
/** 技能分组 → 中文标签(用于选中详情) */
@ -526,7 +526,7 @@ const styles = StyleSheet.create({
// flexShrink: 0 是必需的:搜索框出现后父容器变矮,默认 flexShrink:1 会把这条横向
// ScrollView 压到内容高度以下,胶囊文字被纵向裁掉(点得到、看不见)
tabsWrap: { backgroundColor: colors.white, maxHeight: 52, flexGrow: 0, flexShrink: 0 },
tabs: { flexDirection: 'row', paddingHorizontal: 16, gap: 8, paddingVertical: 10 },
tabs: { flexDirection: 'row', paddingHorizontal: spacing.framework, gap: 8, paddingVertical: 10 },
tab: {
paddingHorizontal: 14,
paddingVertical: 6,
@ -539,7 +539,7 @@ const styles = StyleSheet.create({
filterBar: {
flexDirection: 'row',
backgroundColor: colors.white,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingBottom: 12,
gap: 10,
},
@ -547,7 +547,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.pageBg,
borderRadius: 8,
borderRadius: spacing.card,
paddingHorizontal: 10,
paddingVertical: 5,
},
@ -564,13 +564,13 @@ const styles = StyleSheet.create({
categoryTabActive: { backgroundColor: colors.primaryLight, borderColor: colors.primary },
categoryText: { fontSize: 12, color: colors.textSecondary },
categoryTextActive: { color: colors.primaryText, fontWeight: '600' },
body: { paddingHorizontal: 8, paddingBottom: 32 },
body: { paddingHorizontal: spacing.card, paddingBottom: 32 },
sectionTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 },
cloudCard: {
backgroundColor: colors.white,
borderRadius: 8,
paddingVertical: 8,
paddingHorizontal: 8,
borderRadius: spacing.card,
paddingVertical: spacing.card,
paddingHorizontal: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -579,8 +579,8 @@ const styles = StyleSheet.create({
card: {
marginTop: 8,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -588,7 +588,7 @@ const styles = StyleSheet.create({
skillRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingVertical: spacing.card,
borderTopWidth: 1,
borderTopColor: colors.borderLight,
},
@ -623,8 +623,8 @@ const styles = StyleSheet.create({
matchCard: {
marginTop: 8,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -655,7 +655,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
gap: 8,
backgroundColor: colors.white,
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingBottom: 10,
},
searchInput: {
@ -663,16 +663,16 @@ const styles = StyleSheet.create({
backgroundColor: colors.pageBg,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 8,
paddingVertical: spacing.card,
fontSize: 14,
color: colors.textPrimary,
},
searchEmpty: {
alignItems: 'center',
gap: 10,
padding: 8,
padding: spacing.card,
backgroundColor: colors.white,
borderRadius: 8,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.borderLight,
},
@ -688,18 +688,18 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingHorizontal: spacing.framework,
paddingVertical: 14,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
modalTitle: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
modalCenter: { alignItems: 'center', justifyContent: 'center', gap: 12, paddingVertical: 48 },
modalBody: { paddingHorizontal: 16, paddingBottom: 12 },
modalBody: { paddingHorizontal: spacing.framework, paddingBottom: 12 },
pathSummary: {
backgroundColor: colors.primaryLight,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginTop: 8,
marginBottom: 8,
},
@ -728,8 +728,8 @@ const styles = StyleSheet.create({
detailCard: {
marginTop: 8,
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
borderWidth: 1,
borderColor: colors.successBorder,
},
@ -757,7 +757,7 @@ const styles = StyleSheet.create({
tip: { fontSize: 14, color: colors.textSecondary },
retryBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},

BIN
assets/brand-logo.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/favicon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 383 KiB

After

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/splash-icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

2
components/onboarding/screens/B1Wechat.tsx

@ -32,7 +32,7 @@ export function B1Wechat() {
const styles = StyleSheet.create({
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing.block, paddingBottom: 40 },
preview: { flexDirection: 'row', alignItems: 'center', gap: spacing.block, backgroundColor: colors.white, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: colors.border },
preview: { flexDirection: 'row', alignItems: 'center', gap: spacing.block, backgroundColor: colors.white, padding: spacing.card, borderRadius: spacing.card, borderWidth: 1, borderColor: colors.border },
previewText: { gap: 4 },
nick: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
tip: { fontSize: 12, color: colors.textHint },

6
components/onboarding/screens/P12Company.tsx

@ -1,5 +1,5 @@
import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme';
import { colors , spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell';
import { SearchInput } from '../ui/SearchInput';
@ -34,8 +34,8 @@ export function P12Company() {
const styles = StyleSheet.create({
notice: {
marginTop: 8,
padding: 8,
borderRadius: 8,
padding: spacing.card,
borderRadius: spacing.card,
backgroundColor: colors.primaryLight,
},
noticeText: { fontSize: 12, color: colors.primaryText, lineHeight: 18 },

4
components/onboarding/screens/P17Reference.tsx

@ -80,8 +80,8 @@ const styles = StyleSheet.create({
swapText: { fontSize: 13, color: colors.primary, fontWeight: '600' },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
gap: spacing.block,
borderWidth: 1,
borderColor: colors.border,

2
components/onboarding/screens/P1Login.tsx

@ -76,7 +76,7 @@ export function P1Login() {
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: colors.pageBg },
topGlow: { position: 'absolute', top: 0, left: 0, right: 0, height: 200 },
content: { flexGrow: 1, paddingHorizontal: 8, paddingBottom: spacing.block },
content: { flexGrow: 1, paddingHorizontal: spacing.card, paddingBottom: spacing.block },
brandArea: { alignItems: 'center', marginTop: '22%' },
flexSpacer: { flex: 1, minHeight: 28 },
quickBtn: {

24
components/onboarding/screens/P2PasswordLogin.tsx

@ -7,6 +7,7 @@ import { useAgreementGate, useOnboarding, usePasswordLogin } from '../../../src/
import { isValidPhone } from '../../../src/utils';
import { useAuth } from '../../../src/context/AuthContext';
import { useRole } from '../../../src/context/RoleContext';
import { fetchCurrentUser } from '../../../src/api';
import { PasswordField } from '../../../src/components/PasswordField';
import { ScreenShell } from '../ui/ScreenShell';
import { AgreementCheckbox } from '../ui/AgreementCheckbox';
@ -23,9 +24,9 @@ export function P2PasswordLogin() {
const { data, setData, navigate, back } = useOnboarding();
const { gate, modalProps } = useAgreementGate();
const { login, loading, error } = usePasswordLogin();
/** 登录态写入:换取令牌后必须落库,否则冷启动会被打回登录页 */
const { login: writeAuth } = useAuth();
const { role } = useRole();
/** 双 Token 写入:换取令牌后必须落库,否则冷启动会被打回登录页 */
const { setSession } = useAuth();
const { role, setRole } = useRole();
const router = useRouter();
const [password, setPassword] = useState('');
@ -39,15 +40,26 @@ export function P2PasswordLogin() {
if (!canSubmit || loading) return;
try {
const result = await login(phone, password);
writeAuth(result.token);
// 双 Token 落库(access + refresh),后续请求自动带 Bearer
await setSession(result.accessToken, result.refreshToken, result.expiresAt);
} catch {
// 失败原因由 usePasswordLogin 记录,页面下方展示
return;
}
// 角色以 /auth/me 后端返回为准(规范 §5),不一致时纠正路由
let landingRole = role;
try {
const me = await fetchCurrentUser();
setRole(me.role);
landingRole = me.role;
} catch {
// /auth/me 失败时退回到本地已选身份,不阻断登录
}
// 已有身份的老用户直接进对应主界面,新用户才去选身份
if (role) {
router.replace(role === 'enterprise' ? '/(enterprise)/job-management' : '/(tabs)/jobs');
if (landingRole) {
router.replace(landingRole === 'enterprise' ? '/(enterprise)/job-management' : '/(tabs)/jobs');
return;
}
navigate('P3');

2
components/onboarding/screens/P2PasswordReset.tsx

@ -55,7 +55,7 @@ export function P2PasswordReset() {
const sendCodeNow = async () => {
if (!phoneValid) return;
try {
await sendCode(phone);
await sendCode(phone, 'password-reset');
} catch {
// 失败原因由 useSmsCode 记录,页面下方展示
}

4
components/onboarding/screens/P2PhoneCode.tsx

@ -39,7 +39,7 @@ export function P2PhoneCode() {
const requestCode = async () => {
if (!phoneValid) return;
try {
await sendCode(phone);
await sendCode(phone, 'login');
setStage('code');
} catch {
// 失败原因由 useSmsCode 记录,页面下方统一展示
@ -48,7 +48,7 @@ export function P2PhoneCode() {
/** 阶段2 重新获取(不改变阶段) */
const resendCode = () => {
void sendCode(phone).catch(() => {
void sendCode(phone, 'login').catch(() => {
// 同上,错误已记录
});
};

2
components/onboarding/screens/P2WechatCode.tsx

@ -132,7 +132,7 @@ const styles = StyleSheet.create({
marginTop: 30,
paddingHorizontal: 18,
paddingVertical: spacing.sm,
borderRadius: 8,
borderRadius: spacing.card,
backgroundColor: colors.borderLight,
},
toastText: { fontSize: 13, color: colors.textDisabled },

2
components/onboarding/screens/P3Identity.tsx

@ -70,7 +70,7 @@ export function P3Identity() {
const styles = StyleSheet.create({
heading: { fontSize: 22, fontWeight: '800', color: colors.textPrimary, textAlign: 'center', marginVertical: 16 },
card: { backgroundColor: colors.white, borderRadius: 8, padding: 8, alignItems: 'center', borderWidth: 1, borderColor: colors.border },
card: { backgroundColor: colors.white, borderRadius: spacing.card, padding: spacing.card, alignItems: 'center', borderWidth: 1, borderColor: colors.border },
cardTop: { marginBottom: 16 },
cardBtn: {
width: '100%',

2
components/onboarding/ui/BrandHeader.tsx

@ -14,7 +14,7 @@ export function BrandHeader({ slogan }: { slogan?: string }) {
}
const styles = StyleSheet.create({
wrap: { alignItems: 'center', gap: spacing.block, paddingVertical: 8 },
wrap: { alignItems: 'center', gap: spacing.block, paddingVertical: spacing.card },
name: { fontSize: 26, fontWeight: '800', color: colors.textPrimary },
slogan: { fontSize: 13, color: colors.textHint },
});

4
components/onboarding/ui/CountdownButton.tsx

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { Pressable, StyleSheet, Text } from 'react-native';
import { colors } from '../../../src/theme';
import { colors , spacing } from '../../../src/theme';
interface Props {
disabled?: boolean;
@ -47,7 +47,7 @@ export function CountdownButton({ disabled = false, onSend, seconds = 60 }: Prop
}
const styles = StyleSheet.create({
btn: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 16, backgroundColor: colors.primaryLight },
btn: { paddingHorizontal: 14, paddingVertical: spacing.card, borderRadius: 16, backgroundColor: colors.primaryLight },
btnDisabled: { backgroundColor: colors.borderLight },
text: { fontSize: 13, fontWeight: '600', color: colors.primaryText },
textDisabled: { color: colors.textDisabled },

4
components/onboarding/ui/Modal.tsx

@ -31,8 +31,8 @@ const styles = StyleSheet.create({
card: {
width: '84%',
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
maxHeight: '80%',
},
title: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, marginBottom: spacing.block, textAlign: 'center' },

8
components/onboarding/ui/RadioRow.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme';
import { colors , spacing } from '../../../src/theme';
interface Props {
label: string;
@ -29,9 +29,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 8,
paddingHorizontal: 8,
borderRadius: 8,
paddingVertical: spacing.card,
paddingHorizontal: spacing.card,
borderRadius: spacing.card,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.white,

2
components/onboarding/ui/ScreenShell.tsx

@ -47,6 +47,6 @@ const styles = StyleSheet.create({
titleInline: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
titleCenter: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, textAlign: 'center', paddingVertical: spacing.block },
scroll: { flex: 1 },
scrollContent: { flexGrow: 1, padding: 8, paddingBottom: 28 },
scrollContent: { flexGrow: 1, padding: spacing.card, paddingBottom: 28 },
footer: { padding: 16, paddingBottom: spacing.section, backgroundColor: colors.pageBg },
});

8
components/resume/EditKit.tsx

@ -7,7 +7,7 @@
import { Ionicons } from '@expo/vector-icons';
import type { ReactNode } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../src/theme';
import { colors , spacing } from '../../src/theme';
/** 顶部导航:返回 + 居中标题(右侧留同宽占位,保证标题居中) */
export function EditHeader({ title, onBack }: { title: string; onBack: () => void }) {
@ -176,8 +176,8 @@ const styles = StyleSheet.create({
note: { fontSize: 12, color: colors.textHint, lineHeight: 18 },
card: {
backgroundColor: colors.white,
borderRadius: 8,
padding: 8,
borderRadius: spacing.card,
padding: spacing.card,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.borderLight,
@ -200,7 +200,7 @@ const styles = StyleSheet.create({
centerText: { fontSize: 14, color: colors.textSecondary },
centerBtn: {
paddingHorizontal: 18,
paddingVertical: 8,
paddingVertical: spacing.card,
borderRadius: 16,
backgroundColor: colors.primaryLight,
},

109
docs/联调路径校正映射.md

@ -0,0 +1,109 @@
# 联调路径校正映射表(基于 job.youthome.com.cn OpenAPI 实测)
> 生成日期:2026-09-28 | 数据源:`scripts/openapi.json`(182 条真实路径 / 220 path-methods)
> 审计范围:api.ts 内全部 `request()` 调用(89 个函数)
> 结论:ALIGNED(已对齐)= 6,WRONG→REAL(路径错但有真实等价)= 14,PHANTOM(幽灵域,后端无契约)= 69
## 一、已对齐(无需改路径)
| 函数 | 真实路径 | 原标记 |
|---|---|---|
| `createJobReview` | `POST /jobs/{jobId}/reviews` | 已校正 |
| `fetchCompareData` | `/jobs/compare` | 已校正 |
| `fetchEmployeeReviews` | `GET /jobs/{jobId}/reviews` | 已校正 |
| `fetchJobDetail` | `GET /jobs/{jobId}` | 已校正 |
| `fetchOfferCompareData` | `GET /offers/compare` | 【待确认】 |
| `fetchUserProfile` | `GET /profile/me` | 已校正 |
## 二、路径错但有真实等价端点(需改路径)
| 函数 | 当前(错) | 应改为(真实) | 说明 |
|---|---|---|---|
| `fetchCandidateDetail` | `/enterprise/candidates/${candidateId}` | `/company/candidates/{applicationId}` | 前端猜 /enterprise/candidates,真路径在 /company 域 |
| `fetchConversation` | `/chat/conversations/${id}` | `GET /conversations/{id}` | 前端猜 /chat/conversations,真路径 /conversations/{id} |
| `fetchConversations` | `/messages/conversations` | `GET /conversations` | 前端猜 /messages/conversations,真 /conversations |
| `fetchInterviewScore` | `/enterprise/interviews/${interviewId}/score` | `GET /interviews/{interviewId}/score` | 前端猜 /enterprise/interviews,真路径在 /interviews 域 |
| `fetchInterviewScoreDraft` | `/enterprise/interviews/${interviewId}/score/draft` | `PUT /interviews/{interviewId}/score` | 真路径无 /draft 子路径,保存草稿用 PUT /interviews/{id}/score |
| `fetchInterviews` | `/messages/interviews` | `GET /interviews/me` | 前端猜 /messages/interviews,真 /interviews/me(我的面试列表) |
| `fetchJobPreference` | `/profile/me/job-preference` | `GET/PUT /profile/me/preferences` | 前端猜 /profile/me/job-preference,真路径 /profile/me/preferences |
| `fetchJobProgress` | `/tools/job-progress` | `GET /profile/me/career-progress` | 前端猜 /tools/job-progress,真 /profile/me/career-progress(统一求职进度聚合) |
| `fetchMatchResult` | `/messages/match-result` | `GET /applications/{id}/match 或 /jobs/{jobId}/match` | 前端猜 /messages/match-result,真为投递/岗位匹配诊断 |
| `fetchNotifications` | `/messages/notifications` | `GET /notifications` | 前端猜 /messages/notifications,真 /notifications |
| `fetchRankData` | `/jobs/rank` | `GET /jobs/{jobId}/competition-ranking` | 前端猜 /jobs/rank,真为按岗位竞争力排名,语义不同(需 jobId) |
| `fetchResume` | `/resume` | `GET /profile/me/resume` | 前端猜 /resume,真 /profile/me/resume(下载简历) |
| `saveJobPreference` | `/profile/me/job-preference` | `PUT /profile/me/preferences` | 同上 |
| `saveResumeBasicInfo` | `/resume/basic-info` | `POST /profile/me/resume (multipart)` | 前端猜 /resume/basic-info,真为上传简历文件(multipart),结构化字段在 PUT /profile/me |
## 三、幽灵域(后端 OpenAPI 无契约,必须退回 mock-only)
| 函数 | 当前(幽灵)路径 | 说明 |
|---|---|---|
| `deleteSalaryRecord` | `/salary/records/${id}` | 无 /salary 域,后端未提供 |
| `fetchAIInterviewData` | `/interview/config` | 未命中 OpenAPI |
| `fetchAITools` | `/growth/ai-tools` | 未命中 OpenAPI |
| `fetchAbilityAssessment` | `/growth/ability` | 未命中 OpenAPI |
| `fetchAboutInfo` | `/settings/about` | 未命中 OpenAPI |
| `fetchAccountCancel` | `/settings/account-cancel` | 未命中 OpenAPI |
| `fetchAccountSecurity` | `/settings/account-security` | 未命中 OpenAPI |
| `fetchAppealForm` | `/evaluation/appeal` | 未命中 OpenAPI |
| `fetchArticles` | `/growth/articles` | 未命中 OpenAPI |
| `fetchAuditReport` | `/operation/audit-report` | 未命中 OpenAPI |
| `fetchBadges` | `/user/badges` | 未命中 OpenAPI |
| `fetchDimensionScores` | `/user/dimension-scores` | 未命中 OpenAPI |
| `fetchDndSetting` | `/settings/privacy/dnd` | 未命中 OpenAPI |
| `fetchEnterpriseCertification` | `/enterprise/certification` | 未命中 OpenAPI |
| `fetchEnterpriseCommunication` | `/enterprise/communication` | 未命中 OpenAPI |
| `fetchEnterpriseEntry` | `/user/enterprise-entry` | 未命中 OpenAPI |
| `fetchEnterpriseOverview` | `/enterprise/overview` | 未命中 OpenAPI |
| `fetchGameLevels` | `/game/levels` | 未命中 OpenAPI |
| `fetchGameProgress` | `/game/progress` | 未命中 OpenAPI |
| `fetchHREvaluation` | `/evaluation/hr` | 未命中 OpenAPI |
| `fetchHollandAssessment` | `/growth/holland` | 未命中 OpenAPI |
| `fetchHollandIntro` | `/growth/holland-intro` | 未命中 OpenAPI |
| `fetchIdentityOptions` | `/tools/identity-options` | 未命中 OpenAPI |
| `fetchJobEvaluationEmptyState` | `/enterprise/job-evaluation-empty` | 未命中 OpenAPI |
| `fetchJobEvaluationManagement` | `/enterprise/job-evaluations` | 未命中 OpenAPI |
| `fetchJobManagement` | `/enterprise/job-management` | 未命中 OpenAPI |
| `fetchJobOverallScore` | `/jobs/${id}/score` | 无 /jobs/{id}/score,综合分见 /jobs/{id}/competition-ranking 或 /profile/me/candidate-scoring |
| `fetchJobPublishForm` | `/enterprise/jobs/publish-form` | 未命中 OpenAPI |
| `fetchKeywordBlock` | `/settings/privacy/keywords` | 未命中 OpenAPI |
| `fetchLegalBrand` | `/legal/brand` | 未命中 OpenAPI |
| `fetchLegalConversation` | `/legal/conversations/default` | 未命中 OpenAPI |
| `fetchLegalDocument` | `/settings/legal-document/${id}` | 无 /legal 域,后端未提供 |
| `fetchLegalFAQs` | `/legal/faqs` | 未命中 OpenAPI |
| `fetchLegalMembership` | `/legal/membership` | 未命中 OpenAPI |
| `fetchLegalOrders` | `/legal/orders` | 未命中 OpenAPI |
| `fetchLegalPlans` | `/legal/plans` | 未命中 OpenAPI |
| `fetchLegalServiceRequests` | `/legal/service-requests` | 未命中 OpenAPI |
| `fetchLegalServices` | `/legal/services` | 未命中 OpenAPI |
| `fetchLegalTemplates` | `/legal/templates` | 未命中 OpenAPI |
| `fetchMenuItems` | `/user/menus` | 未命中 OpenAPI |
| `fetchNotificationSettings` | `/settings/notification` | 未命中 OpenAPI |
| `fetchOperationRules` | `/operation/rules` | 未命中 OpenAPI |
| `fetchOverallScore` | `/user/overall-score` | 未命中 OpenAPI |
| `fetchPendingOfferCount` | `/offers/pending/count` | 无 /offers/pending/count,可用 /offers/statistics 替代(需确认) |
| `fetchPersonalDataExport` | `/settings/personal-data-export` | 未命中 OpenAPI |
| `fetchPrivacyCenter` | `/settings/privacy-center` | 未命中 OpenAPI |
| `fetchPrivacyDetailSetting` | `/settings/privacy/${id}` | 无 /settings 域,后端未提供 |
| `fetchPrivacySettings` | `/settings/privacy` | 未命中 OpenAPI |
| `fetchQualifications` | `/settings/qualifications` | 未命中 OpenAPI |
| `fetchResumeExportSettings` | `/resume/export-settings` | 无 /resume/export-settings,导出设置本地维护 |
| `fetchResumeSavedAt` | `/resume/saved-at` | 无 /resume/saved-at,简历保存时间可由 /profile/me/resume-versions 派生 |
| `fetchSalaryBrackets` | `/tools/salary-brackets` | 未命中 OpenAPI |
| `fetchSalaryData` | `/jobs/${id}/salary` | 无 /jobs/{id}/salary,后端未提供薪资明细端点 |
| `fetchSalaryRecords` | `/salary/records` | 未命中 OpenAPI |
| `fetchSalaryToolLinks` | `/tools/salary-related` | 未命中 OpenAPI |
| `fetchSkillCloud` | `/tools/skill-cloud` | 未命中 OpenAPI |
| `fetchSkillLearningPath` | `/tools/skill-learning-path` | 未命中 OpenAPI |
| `fetchTalentRecommendation` | `/enterprise/talent-recommendation` | 未命中 OpenAPI |
| `fetchUserStats` | `/user/stats` | 未命中 OpenAPI |
| `fetchWeightSettings` | `/jobs/weight-settings` | 无 /jobs/weight-settings,权重见 /profile/me/review-weights、/review-weight-templates |
| `saveResumeEducation` | `/resume/education` | 无 /resume/education,简历整体由 /profile/me/resume + resume-versions 管理 |
| `saveResumeExperience` | `/resume/experience` | 同上 |
| `saveResumeExportSettings` | `/resume/export-settings` | 未命中 OpenAPI |
| `saveResumeProjects` | `/resume/projects` | 同上 |
| `saveResumeSelfEvaluation` | `/resume/self-evaluation` | 同上 |
| `saveResumeSkills` | `/resume/skills` | 同上 |
| `saveResumeSnapshot` | `/resume/save` | 同上 |
| `saveSalaryRecord` | `/salary/records` | 无 /salary/records |
| `saveWeightSettings` | `/weight-settings` | 无 /weight-settings |

83
scripts/audit_all_paths.py

@ -0,0 +1,83 @@
import json, re
with open("scripts/openapi.json", encoding="utf-8") as f:
spec = json.load(f)
real_paths = set()
for p, methods in spec.get("paths", {}).items():
for m in methods:
real_paths.add(p)
# Load api.ts
with open("src/api/api.ts", encoding="utf-8") as f:
text = f.read()
lines = text.splitlines()
func_re = re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(")
# static literal request path
static_req = re.compile(r"request(?:<[^>]*>)?\(\s*['\"]([^'\"]+)['\"]")
# template literal request path like request(`/x/${..}`)
tmpl_req = re.compile(r"request(?:<[^>]*>)?\(\s*`([^`]*)`")
cur = None
funcs = {} # name -> {static:[], tmpl:[], dai:bool, line}
for i, ln in enumerate(lines):
m = func_re.match(ln)
if m:
cur = m.group(1)
funcs[cur] = {"static": [], "tmpl": [], "dai": False, "line": i+1}
if cur:
for sm in static_req.finditer(ln):
funcs[cur]["static"].append(sm.group(1))
for tm in tmpl_req.finditer(ln):
funcs[cur]["tmpl"].append(tm.group(1))
if "【待确认】" in ln:
funcs[cur]["dai"] = True
# Determine if a guessed path has ANY real backing (prefix match ignoring method)
def has_real_backing(path):
# exact
if path in real_paths:
return True
# base segment match: first 2 segments
segs = [s for s in path.split("/") if s and not "{" in s and not "$" in s]
if len(segs) >= 1:
base = "/" + segs[0]
if any(rp.startswith(base + "/") or rp == base for rp in real_paths):
return "partial"
return False
phantom_static = []
ok_static = []
partial_static = []
tmpl_funcs = []
for name, info in funcs.items():
if not info["static"] and not info["tmpl"]:
continue
for sp in info["static"]:
b = has_real_backing(sp)
if b is True:
ok_static.append((name, sp))
elif b == "partial":
partial_static.append((name, sp))
else:
phantom_static.append((name, sp))
for tp in info["tmpl"]:
tmpl_funcs.append((name, tp))
print("DISTINCT FUNCTIONS WITH request():", sum(1 for n,i in funcs.items() if i["static"] or i["tmpl"]))
print()
print("=== PHANTOM STATIC PATHS (no OpenAPI backing at all) ===")
for n, p in sorted(set(phantom_static)):
dai = "【待确认】" if funcs[n]["dai"] else "(已标'已校正'但仍是幽灵!)"
print(f" {n:42} -> {p:48} {dai}")
print(f"\nPHANTOM count (unique fn-path): {len(set(phantom_static))}")
print(f"OK count: {len(set(ok_static))}")
print(f"PARTIAL count: {len(set(partial_static))}")
print()
print("=== PARTIAL (base domain exists, path differs) ===")
for n, p in sorted(set(partial_static)):
print(f" {n:42} -> {p}")
print()
print("=== TEMPLATE-LITERAL request paths (need manual method check) ===")
for n, p in sorted(set(tmpl_funcs)):
print(f" {n:42} -> {p}")

119
scripts/docx_text.txt

@ -0,0 +1,119 @@
职来后端接口联调说明
前端开发使用 线上联调环境 2026年9月22日
结论:前端统一通过 HTTPS 域名访问后端,业务请求前缀为 https://job.youthome.com.cn/api/v1。除注册、登录、短信验证码、刷新 Token 等匿名接口外,其余接口需要在 Authorization 请求头携带 Access Token。
一 联调地址
[TABLE]
用途 | 地址 | 使用说明
业务接口基地址 | https://job.youthome.com.cn/api/v1 | 前端环境变量中的 API Base URL
健康检查 | https://job.youthome.com.cn/api/v1/health | 返回 code=200 且 data.status=UP 表示后端可用
OpenAPI JSON | https://job.youthome.com.cn/api/v1/v3/api-docs | Apifox 导入或更新接口定义使用
WebSocket | wss://job.youthome.com.cn/api/v1/ws | 仅在对应实时功能启用时使用;不要使用 ws://
重要:不要直接请求服务器 IP、Jenkins 地址或 8010 端口。前端、Apifox 和浏览器都统一使用 HTTPS 域名,避免跨域、证书和环境切换问题。
二 Apifox 配置方法
在 Apifox 项目中选择导入数据,导入方式选 URL,填写 OpenAPI JSON 地址。
环境前置 URL 设置为 https://job.youthome.com.cn/api/v1。接口路径本身只保留 /auth/me、/jobs 等相对路径。
创建环境变量 AuthToken,用登录响应中的 accessToken 赋值。
需要认证的接口统一设置请求头 Authorization: Bearer {{AuthToken}}。
接口有调整时重新从 OpenAPI URL 增量导入,不要手工复制旧接口覆盖新定义。
三 通用请求和响应规则
[TABLE]
项目 | 规则
请求格式 | 普通业务接口使用 application/json;上传简历等文件接口使用 multipart/form-data。
认证请求头 | Authorization: Bearer <accessToken>。Refresh Token 不能代替 Access Token 访问业务接口。
请求追踪 | 可传 X-Request-Id;后端会在响应头和响应体返回 requestId,报错联调时请一并提供。
时间格式 | 按 OpenAPI 定义发送 ISO 8601 日期或日期时间,不要自行转换为中文日期。
分页 | 以接口定义中的 page、size 为准;不要在前端假设页码起点。
响应外层 | 通常为 code、message、data、requestId、timestamp。业务数据读取 data。
Authorization: Bearer {{AuthToken}}Content-Type: application/jsonX-Request-Id: web-20260922-0001
{ "code": 200, "message": "ok", "data": { }, "requestId": "...", "timestamp": "..."}
四 登录与会话使用顺序
[TABLE]
步骤 | 接口 | 前端处理
1 | POST /auth/sms/send | 按 purpose 发送验证码;purpose 必须使用后端支持的固定值。
2A | POST /auth/password/login | 手机号和密码登录;成功后保存 accessToken、refreshToken。
2B | POST /auth/login | 短信验证码登录;现接口文档标为 Mock 登录,生产前端不要作为正式入口,除非产品和后端明确确认。
3 | GET /auth/me | 登录后拉取用户 id、角色和当前身份信息。
4 | POST /auth/refresh | Access Token 过期时,用 Refresh Token 换取新 Token。
5 | POST /auth/logout | 退出登录;成功后清除前端保存的所有 Token 和用户状态。
Token 保存与刷新建议
Access Token 用于业务请求;Refresh Token 只提交给 /auth/refresh 和会话相关接口。
遇到 401 时只允许一次串行刷新;刷新成功后重放原请求,刷新失败则清理会话并跳转登录页。
不要把 Token 打印到控制台、错误上报、URL 查询参数或页面 DOM。
不同浏览器标签页同时刷新时要加前端互斥,避免多个 Refresh Token 并发重放。
五 学生 企业 管理员的区别
[TABLE]
身份 | 账号产生方式 | 登录方式 | 主要接口前缀
STUDENT | 现有短信登录流程可自动创建学生账号 | 短信登录或已设置密码后的密码登录 | /profile/me、/applications、/offers、/interviews
COMPANY | POST /auth/company/register 专用注册 | 注册成功直接返回 Token;之后可密码登录 | /companies/me、/company、/jobs
ADMIN | 不提供公开注册,由管理员初始化或后台受控创建 | 使用现有认证方式登录 | /admin
前端路由判断:登录成功后以 /auth/me 返回的角色为准。不要让客户端提交 role 来决定身份,也不要仅依据页面入口猜测用户角色。
六 企业注册与入驻流程
[TABLE]
顺序 | 接口 | 预期结果
1 | POST /auth/sms/send | purpose 使用 company-register,获取企业注册验证码。
2 | POST /auth/company/register | 提交手机号、验证码、密码和企业基础资料;成功返回 COMPANY Token。
3 | GET /auth/me | 确认当前角色为 COMPANY。
4 | GET /companies/me/onboarding | 读取 UNVERIFIED、PENDING、REJECTED 或 VERIFIED,以及 missingFields、allowedActions。
5 | PUT /companies/me | 根据 missingFields 完善企业资料。
6 | POST /companies/me/verification | 提交企业认证材料。
7 | GET /companies/me/onboarding | 刷新聚合状态;前端按钮以 allowedActions 为准。
企业注册请求示例
POST /auth/company/register{ "phone": "19900000000", "code": "123456", "password": "符合密码策略的密码", "company": { "name": "示例科技有限公司", "industry": "互联网", "description": "企业简介", "website": "https://example.com", "address": "企业地址" }}
注意:客户端不能提交 userId、role、verified 或 passwordHash。注册响应中的 accessToken 和 refreshToken 按普通登录响应处理。
七 常用业务接口导航
[TABLE]
业务模块 | 常用接口 | 认证要求
公开岗位 | GET /jobs、GET /jobs/{jobId}、GET /jobs/search、GET /jobs/filters | 公开读取;具体以 OpenAPI security 定义为准
学生资料 | GET/PUT /profile/me、POST/GET/DELETE /profile/me/resume | STUDENT
求职流程 | 投递、面试、Offer、消息等接口 | 登录后按角色和资源归属校验
企业资料 | GET/PUT /companies/me | COMPANY
企业入驻 | GET /companies/me/onboarding、POST/GET /companies/me/verification | COMPANY
企业岗位 | POST/PUT /jobs 及发布关闭接口 | COMPANY;部分能力可能要求完成认证
管理员 | /admin 下审核、风控、举报等接口 | ADMIN
完整字段、枚举、必填项、响应结构和全部接口以运行时 OpenAPI 为唯一联调依据。本文只提供入口和使用顺序。
八 常见状态码处理
[TABLE]
HTTP | 含义 | 前端建议
400 | 参数错误、验证码错误或业务输入不合法 | 展示安全业务文案;字段错误定位到表单项。
401 | 未登录、Token 无效或过期 | 尝试一次刷新;失败后清理会话并回登录页。
403 | 角色不允许或无资源权限 | 展示无权限页面,不要循环刷新 Token。
404 | 资源不存在 | 展示资源不存在或返回列表页。
409 | 账号、状态或并发冲突 | 刷新当前数据后提示用户重新操作。
429 | 请求过于频繁 | 禁用按钮并显示倒计时,不要立即自动重试。
500 | 服务内部错误 | 显示通用错误,并上报 requestId。
503 | 依赖服务不可用或功能暂不可用 | 提示稍后重试,避免高频轮询。
九 前端联调检查清单
环境 Base URL 仅配置一次,接口路径中不重复拼接 /api/v1。
登录后 /auth/me 能返回正确角色,Authorization 头格式正确。
401 刷新流程不会无限循环,403 不触发刷新。
企业注册先发送 company-register 用途验证码,再调用企业注册接口。
企业页面从 /companies/me/onboarding 获取状态和 allowedActions,不在前端自行推导认证状态。
上传接口使用 multipart/form-data,不手动覆盖浏览器生成的 boundary。
报错时保留 HTTP 状态、业务 code、message、requestId 和请求时间;删除 Token、手机号、验证码及密码后再发给后端排查。
联调前先访问健康检查;健康为 UP 后再判断具体业务接口问题。
十 安全和协作约定
禁止在前端代码、Git、群聊截图或 Apifox 公共变量中保存数据库、Redis、RabbitMQ、Jenkins 密码。
禁止把 Access Token、Refresh Token、短信验证码和用户密码写入日志或错误上报。
接口字段调整由后端更新 OpenAPI,前端重新导入;不要以聊天记录替代接口定义。
需要后端排查时提供:环境、接口、方法、HTTP 状态、业务 code、requestId、发生时间和脱敏请求。

36
scripts/extract_docx.py

@ -0,0 +1,36 @@
import zipfile, sys
from xml.etree import ElementTree as ET
path = "职来后端接口联调说明.docx"
z = zipfile.ZipFile(path)
xml = z.read("word/document.xml").decode("utf-8")
ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}
root = ET.fromstring(xml)
def para_text(p):
texts = []
for t in p.iter("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t"):
texts.append(t.text or "")
return "".join(texts)
body = root.find("w:body", ns)
out = []
for el in body:
tag = el.tag.split("}")[-1]
if tag == "p":
out.append(para_text(el))
elif tag == "tbl":
rows = []
for tr in el.iter("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tr"):
cells = [para_text(p) for p in tr.iter("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p")]
rows.append(" | ".join(cells))
out.append("\n[TABLE]\n" + "\n".join(rows) + "\n")
full = "\n".join(out)
with open("scripts/docx_text.txt", "w", encoding="utf-8") as f:
f.write(full)
print("TOTAL CHARS:", len(full))
for i, line in enumerate(out):
if any(k in line for k in ["OpenAPI", "接口", "路径", "endpoint", "/api", "GET", "POST", "§7", "七、"]):
print(f"[{i}] {line[:140]}")

156
scripts/gen_correction_map.py

@ -0,0 +1,156 @@
import json, re
with open("scripts/openapi.json", encoding="utf-8") as f:
spec = json.load(f)
real_paths = set()
real_path_methods = {}
for p, methods in spec.get("paths", {}).items():
real_paths.add(p)
for m in methods:
real_path_methods.setdefault(p, []).append(m.upper())
with open("src/api/api.ts", encoding="utf-8") as f:
lines = f.readlines()
func_re = re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(")
static_req = re.compile(r"request(?:<[^>]*>)?\(\s*['\"]([^'\"]+)['\"]")
tmpl_req = re.compile(r"request(?:<[^>]*>)?\(\s*`([^`]*)`")
dai_re = re.compile(r"【待确认】")
cur = None
funcs = {}
for i, ln in enumerate(lines):
m = func_re.match(ln)
if m:
cur = m.group(1)
funcs[cur] = {"static": [], "tmpl": [], "dai": False, "line": i+1}
if cur:
for sm in static_req.finditer(ln):
funcs[cur]["static"].append(sm.group(1))
for tm in tmpl_req.finditer(ln):
funcs[cur]["tmpl"].append(tm.group(1))
if dai_re.search(ln):
funcs[cur]["dai"] = True
# Manual real-equivalent judgment for template/partial/path-wrong functions
# value: (verdict, real_path_or_None, note)
manual = {
# template literals - verify
"createJobReview": ("ALIGNED", "POST /jobs/{jobId}/reviews", "路径命中真实契约"),
"fetchEmployeeReviews": ("ALIGNED", "GET /jobs/{jobId}/reviews", "路径命中真实契约"),
"fetchJobDetail": ("ALIGNED", "GET /jobs/{jobId}", "路径命中真实契约"),
"fetchCandidateDetail": ("WRONG->REAL", "/company/candidates/{applicationId}", "前端猜 /enterprise/candidates,真路径在 /company 域"),
"fetchInterviewScore": ("WRONG->REAL", "GET /interviews/{interviewId}/score", "前端猜 /enterprise/interviews,真路径在 /interviews 域"),
"fetchInterviewScoreDraft": ("WRONG->REAL", "PUT /interviews/{interviewId}/score", "真路径无 /draft 子路径,保存草稿用 PUT /interviews/{id}/score"),
"fetchConversation": ("WRONG->REAL", "GET /conversations/{id}", "前端猜 /chat/conversations,真路径 /conversations/{id}"),
"deleteSalaryRecord": ("PHANTOM", None, "无 /salary 域,后端未提供"),
"fetchLegalDocument": ("PHANTOM", None, "无 /legal 域,后端未提供"),
"fetchPrivacyDetailSetting": ("PHANTOM", None, "无 /settings 域,后端未提供"),
"fetchSalaryData": ("PHANTOM", None, "无 /jobs/{id}/salary,后端未提供薪资明细端点"),
"fetchJobOverallScore": ("PHANTOM", None, "无 /jobs/{id}/score,综合分见 /jobs/{id}/competition-ranking 或 /profile/me/candidate-scoring"),
# partial static
"fetchJobPreference": ("WRONG->REAL", "GET/PUT /profile/me/preferences", "前端猜 /profile/me/job-preference,真路径 /profile/me/preferences"),
"saveJobPreference": ("WRONG->REAL", "PUT /profile/me/preferences", "同上"),
"fetchPendingOfferCount": ("PHANTOM", None, "无 /offers/pending/count,可用 /offers/statistics 替代(需确认)"),
"fetchRankData": ("WRONG->REAL", "GET /jobs/{jobId}/competition-ranking", "前端猜 /jobs/rank,真为按岗位竞争力排名,语义不同(需 jobId)"),
"fetchWeightSettings": ("PHANTOM", None, "无 /jobs/weight-settings,权重见 /profile/me/review-weights、/review-weight-templates"),
# already-correct static
"fetchJobList": ("ALIGNED", "GET /jobs", "命中真实契约"),
"fetchOfferCompareData": ("ALIGNED", "GET /offers/compare", "命中真实契约"),
"fetchUserProfile": ("ALIGNED", "GET /profile/me", "命中真实契约"),
# path-wrong (phantom base) that have real equivalents elsewhere
"fetchConversations": ("WRONG->REAL", "GET /conversations", "前端猜 /messages/conversations,真 /conversations"),
"fetchNotifications": ("WRONG->REAL", "GET /notifications", "前端猜 /messages/notifications,真 /notifications"),
"fetchInterviews": ("WRONG->REAL", "GET /interviews/me", "前端猜 /messages/interviews,真 /interviews/me(我的面试列表)"),
"fetchMatchResult": ("WRONG->REAL", "GET /applications/{id}/match 或 /jobs/{jobId}/match", "前端猜 /messages/match-result,真为投递/岗位匹配诊断"),
"fetchResume": ("WRONG->REAL", "GET /profile/me/resume", "前端猜 /resume,真 /profile/me/resume(下载简历)"),
"fetchFavorites": ("WRONG->REAL", "GET /jobs/favorites", "前端猜 /profile/me/favorites,真 /jobs/favorites"),
"fetchJobProgress": ("WRONG->REAL", "GET /profile/me/career-progress", "前端猜 /tools/job-progress,真 /profile/me/career-progress(统一求职进度聚合)"),
"fetchResumeSavedAt": ("PHANTOM", None, "无 /resume/saved-at,简历保存时间可由 /profile/me/resume-versions 派生"),
"fetchResumeExportSettings": ("PHANTOM", None, "无 /resume/export-settings,导出设置本地维护"),
"saveResumeBasicInfo": ("WRONG->REAL", "POST /profile/me/resume (multipart)", "前端猜 /resume/basic-info,真为上传简历文件(multipart),结构化字段在 PUT /profile/me"),
"saveResumeEducation": ("PHANTOM", None, "无 /resume/education,简历整体由 /profile/me/resume + resume-versions 管理"),
"saveResumeExperience": ("PHANTOM", None, "同上"),
"saveResumeProjects": ("PHANTOM", None, "同上"),
"saveResumeSelfEvaluation": ("PHANTOM", None, "同上"),
"saveResumeSkills": ("PHANTOM", None, "同上"),
"saveResumeSnapshot": ("PHANTOM", None, "同上"),
"saveSalaryRecord": ("PHANTOM", None, "无 /salary/records"),
"saveWeightSettings": ("PHANTOM", None, "无 /weight-settings"),
}
def base_has_real(p):
segs = [s for s in p.split("/") if s and "{" not in s and "$" not in s]
if not segs:
return False
base = "/" + segs[0]
return any(rp == base or rp.startswith(base + "/") for rp in real_paths)
verdicts = {}
for name, info in funcs.items():
if not info["static"] and not info["tmpl"]:
continue
if name in manual:
v, rp, note = manual[name]
verdicts[name] = (v, rp, note, info["dai"], info["line"])
continue
# auto
paths = info["static"] + info["tmpl"]
# for static-only auto
allphantom = True
anyok = False
for p in info["static"]:
if p in real_paths:
anyok = True; allphantom = False
elif base_has_real(p):
allphantom = False
if anyok:
verdicts[name] = ("ALIGNED", ", ".join(info["static"]), "命中真实契约", info["dai"], info["line"])
elif allphantom is False and info["static"]:
verdicts[name] = ("PHANTOM", ", ".join(info["static"]), "父域不存在于 OpenAPI", info["dai"], info["line"])
else:
verdicts[name] = ("PHANTOM", ", ".join(paths), "未命中 OpenAPI", info["dai"], info["line"])
# also catch functions flagged dai but no request captured (e.g. buildPrivacySummaryText) - skip
order = {"ALIGNED":0, "WRONG->REAL":1, "PHANTOM":2}
rows = sorted(verdicts.items(), key=lambda kv: (order.get(kv[1][0],9), kv[0]))
from collections import Counter
c = Counter(v[0] for v in verdicts.values())
print("SUMMARY:", dict(c), "TOTAL FUNCS MAPPED:", len(verdicts))
md = ["# 联调路径校正映射表(基于 job.youthome.com.cn OpenAPI 实测)", ""]
md.append(f"> 生成日期:2026-09-28 | 数据源:`scripts/openapi.json`(182 条真实路径 / 220 path-methods)")
md.append(f"> 审计范围:api.ts 内全部 `request()` 调用({len(verdicts)} 个函数)")
md.append(f"> 结论:ALIGNED(已对齐)= {c.get('ALIGNED',0)},WRONG→REAL(路径错但有真实等价)= {c.get('WRONG->REAL',0)},PHANTOM(幽灵域,后端无契约)= {c.get('PHANTOM',0)}")
md.append("")
md.append("## 一、已对齐(无需改路径)")
md.append("")
md.append("| 函数 | 真实路径 | 原标记 |")
md.append("|---|---|---|")
for n,(v,rp,note,dai,line) in rows:
if v=="ALIGNED":
md.append(f"| `{n}` | `{rp}` | {'【待确认】' if dai else '已校正'} |")
md.append("")
md.append("## 二、路径错但有真实等价端点(需改路径)")
md.append("")
md.append("| 函数 | 当前(错) | 应改为(真实) | 说明 |")
md.append("|---|---|---|---|")
for n,(v,rp,note,dai,line) in rows:
if v=="WRONG->REAL":
curpath = ", ".join(funcs[n]["static"]+funcs[n]["tmpl"])
md.append(f"| `{n}` | `{curpath}` | `{rp}` | {note} |")
md.append("")
md.append("## 三、幽灵域(后端 OpenAPI 无契约,必须退回 mock-only)")
md.append("")
md.append("| 函数 | 当前(幽灵)路径 | 说明 |")
md.append("|---|---|---|")
for n,(v,rp,note,dai,line) in rows:
if v=="PHANTOM":
curpath = ", ".join(funcs[n]["static"]+funcs[n]["tmpl"])
md.append(f"| `{n}` | `{curpath}` | {note} |")
md.append("")
with open("docs/联调路径校正映射.md", "w", encoding="utf-8") as f:
f.write("\n".join(md))
print("WROTE docs/联调路径校正映射.md")

76
scripts/live_validate.py

@ -0,0 +1,76 @@
import json, urllib.request, urllib.error
BASE = "https://job.youthome.com.cn/api/v1"
def req(method, path, token=None, body=None):
url = BASE + path
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(url, data=data, method=method)
r.add_header("Content-Type", "application/json")
if token:
r.add_header("Authorization", "Bearer " + token)
try:
with urllib.request.urlopen(r, timeout=20) as resp:
raw = resp.read().decode()
status = resp.status
except urllib.error.HTTPError as e:
raw = e.read().decode(errors="replace")
status = e.code
except Exception as e:
return None, str(e), None
try:
j = json.loads(raw)
except Exception:
return status, raw[:200], None
return status, j, raw
# 1) login
s, j, _ = req("POST", "/auth/password/login", body={"phone": "19900009021", "password": "ZhilaiTest!2026#A"})
token = (j or {}).get("data", {}).get("accessToken") if j else None
print("LOGIN:", s, "hasToken=", bool(token), "role=", (j or {}).get("data", {}).get("role"), "expiresIn=", (j or {}).get("data", {}).get("en"))
print("="*100)
# 2) validate ID-less GET endpoints (student token)
idless = [
("GET", "/jobs", "fetchJobList(ALIGNED)"),
("GET", "/offers/compare", "fetchOfferCompareData(ALIGNED)"),
("GET", "/profile/me", "fetchUserProfile(ALIGNED)"),
("GET", "/conversations", "fetchConversations(WRONG->REAL /conversations)"),
("GET", "/notifications", "fetchNotifications(WRONG->REAL /notifications)"),
("GET", "/notifications/unread-count", "bonus real"),
("GET", "/interviews/me", "fetchInterviews(WRONG->REAL /interviews/me)"),
("GET", "/jobs/favorites", "fetchFavorites(WRONG->REAL /jobs/favorites)"),
("GET", "/profile/me/career-progress", "fetchJobProgress(WRONG->REAL /profile/me/career-progress)"),
("GET", "/profile/me/preferences", "fetchJobPreference(WRONG->REAL /profile/me/preferences)"),
("GET", "/profile/me/resume", "fetchResume(WRONG->REAL /profile/me/resume)"),
("GET", "/profile/me/candidate-scoring", "bonus real"),
("GET", "/applications/me", "bonus real /applications/me"),
("GET", "/assessments/me", "bonus real /assessments/me"),
("GET", "/abilities/me", "bonus real /abilities/me"),
]
for m, p, label in idless:
s, j, _ = req(m, p, token)
code = (j or {}).get("code") if isinstance(j, dict) else None
data = (j or {}).get("data")
dtype = type(data).__name__
keys = list(data.keys())[:12] if isinstance(data, dict) else (f"len={len(data)}" if isinstance(data, list) else None)
print(f"{m:4} {p:42} [{label}]")
print(f" http={s} code={code} data_type={dtype} sample={keys}")
print("="*100)
# 3) ID-needing endpoints: best-effort with placeholder id (status ambiguous if 404)
idneed = [
("GET", "/jobs/1", "fetchJobDetail(ALIGNED) needs jobId"),
("GET", "/jobs/1/reviews", "fetchEmployeeReviews(ALIGNED)"),
("GET", "/jobs/1/competition-ranking", "fetchRankData(WRONG->REAL)"),
("GET", "/jobs/1/match", "fetchMatchResult alt"),
("GET", "/conversations/1", "fetchConversation(WRONG->REAL /conversations/{id})"),
("GET", "/applications/1/match", "fetchMatchResult(WRONG->REAL)"),
("GET", "/company/candidates/1", "fetchCandidateDetail(WRONG->REAL, ENTERPRISE role)"),
("GET", "/interviews/1/score", "fetchInterviewScore(WRONG->REAL)"),
]
for m, p, label in idneed:
s, j, _ = req(m, p, token)
code = (j or {}).get("code") if isinstance(j, dict) else None
msg = (j or {}).get("message") if isinstance(j, dict) else None
print(f"{m:4} {p:42} [{label}] http={s} code={code} msg={msg}")

1
scripts/openapi.json

File diff suppressed because one or more lines are too long

20
scripts/openapi_paths.py

@ -0,0 +1,20 @@
import json
with open("scripts/openapi.json", encoding="utf-8") as f:
spec = json.load(f)
paths = spec.get("paths", {})
rows = []
for p, methods in sorted(paths.items()):
for m, op in methods.items():
if m.lower() not in ("get","post","put","delete","patch"):
continue
summary = op.get("summary") or op.get("description") or ""
tags = ",".join(op.get("tags", []))
rows.append((m.upper(), p, summary, tags))
print(f"TOTAL PATH-METHODS: {len(rows)}")
print(f"TOTAL DISTINCT PATHS: {len(set(r[1] for r in rows))}")
print("="*100)
for m, p, s, t in rows:
print(f"{m:6} {p:55} [{t}] {s[:40]}")

188
scripts/transform_api.py

@ -0,0 +1,188 @@
import json, re
with open("scripts/openapi.json", encoding="utf-8") as f:
spec = json.load(f)
real_paths = set()
for p, methods in spec.get("paths", {}).items():
for m in methods:
real_paths.add(p)
real_base = set()
for p in real_paths:
seg = [s for s in p.split("/") if s and "{" not in s and "$" not in s]
if seg:
real_base.add("/" + seg[0])
with open("src/api/api.ts", encoding="utf-8") as f:
raw = f.read()
lines = raw.split("\n") # keep as list, we will rejoin
func_re = re.compile(r"^(\s*)(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(")
static_req = re.compile(r"request(?:<[^>]*>)?\(\s*['\"]([^'\"]+)['\"]")
tmpl_req = re.compile(r"request(?:<[^>]*>)?\(\s*`([^`]*)`")
# find function start indices
starts = []
for i, ln in enumerate(lines):
m = func_re.match(ln)
if m:
starts.append((m.group(2), i))
func_ranges = []
for idx, (name, s) in enumerate(starts):
e = starts[idx+1][1] if idx+1 < len(starts) else len(lines)
func_ranges.append((name, s, e))
PATH_FIX_STATIC = {
"fetchConversations": ("'/messages/conversations'", "'/conversations'"),
"fetchNotifications": ("'/messages/notifications'", "'/notifications'"),
"fetchInterviews": ("'/messages/interviews'", "'/interviews/me'"),
"fetchJobPreference": ("'/profile/me/job-preference'", "'/profile/me/preferences'"),
"saveJobPreference": ("'/profile/me/job-preference'", "'/profile/me/preferences'"),
"fetchResume": ("'/resume'", "'/profile/me/resume'"),
"fetchFavorites": ("'/profile/me/favorites'", "'/jobs/favorites'"),
}
JOBPROGRESS_OLD = "'/tools/job-progress', { method: 'GET', auth: false }"
JOBPROGRESS_NEW = "'/profile/me/career-progress', { method: 'GET' }"
TEMPLATE_FIX = {
"fetchRankData": ("/jobs/rank", "/jobs/${jobId}/competition-ranking"),
"fetchCandidateDetail": ("/enterprise/candidates/${candidateId}", "/company/candidates/${candidateId}"),
"fetchInterviewScore": ("/enterprise/interviews/${interviewId}/score", "/interviews/${interviewId}/score"),
"fetchInterviewScoreDraft": ("/enterprise/interviews/${interviewId}/score/draft", "/interviews/${interviewId}/score"),
"fetchConversation": ("/chat/conversations/${id}", "/conversations/${id}"),
}
def get_info(sl):
static = []; tmpl = []
hmi = any("if (isMockMode())" in l for l in sl)
hnmi = any("if (!isMockMode())" in l for l in sl)
for l in sl:
m = static_req.search(l)
if m: static.append(m.group(1))
t = tmpl_req.search(l)
if t: tmpl.append(t.group(1))
return static, tmpl, hmi, hnmi
def is_phantom(p):
if p in real_paths: return False
segs = [s for s in p.split("/") if s and "{" not in s and "$" not in s]
base = "/" + segs[0] if segs else ""
return base not in real_base
DAI_RE = re.compile(r'【待确认[^】]*】')
def strip_dai(line):
if DAI_RE.search(line):
return DAI_RE.sub('', line).rstrip() + "\n"
return line
changed = {"pathfix": [], "revert": [], "strip_only": []}
new_full = []
def find_close(sl, open_i):
depth = 0
for j in range(open_i, len(sl)):
depth += sl[j].count("{") - sl[j].count("}")
if depth == 0 and j > open_i:
return j
return None
# preserve leading lines before the first function (imports, top-level consts, banners)
new_full = list(lines[0:func_ranges[0][1]])
for name, s, e in func_ranges:
sl = lines[s:e]
static, tmpl, hmi, hnmi = get_info(sl)
in_openapi = any(p in real_paths for p in static) or any(not is_phantom(t) for t in tmpl)
dai = any("【待确认】" in l for l in sl)
is_pathfix = name in PATH_FIX_STATIC or name in TEMPLATE_FIX or name == "fetchJobProgress"
phantom = (dai or any(is_phantom(p) for p in static) or any(is_phantom(t) for t in tmpl)) and not in_openapi
if is_pathfix:
out = []
for ln in sl:
if name in PATH_FIX_STATIC:
o, n = PATH_FIX_STATIC[name]
if o in ln: ln = ln.replace(o, n)
ou = o.strip("'\"")
nu = n.strip("'\"")
if ou in ln: ln = ln.replace(ou, nu)
elif name == "fetchJobProgress":
if JOBPROGRESS_OLD in ln: ln = ln.replace(JOBPROGRESS_OLD, JOBPROGRESS_NEW)
if "/tools/job-progress" in ln: ln = ln.replace("/tools/job-progress", "/profile/me/career-progress")
elif name in TEMPLATE_FIX:
o, n = TEMPLATE_FIX[name]
if o in ln: ln = ln.replace(o, n)
ou = o.replace("`", "")
nu = n.replace("`", "")
if ou in ln: ln = ln.replace(ou, nu)
out.append(strip_dai(ln))
new_full.extend(out)
changed["pathfix"].append(name)
continue
if not phantom:
if dai:
new_full.extend(strip_dai(ln) for ln in sl)
else:
new_full.extend(sl)
continue
# REVERT (phantom -> always mock)
if hnmi:
out = []
i = 0
while i < len(sl):
if "if (!isMockMode())" in sl[i]:
ci = find_close(sl, i)
i = (ci + 1) if ci is not None else i + 1
continue
out.append(strip_dai(sl[i]))
i += 1
new_full.extend(out)
changed["revert"].append(name)
else:
# pattern A: collapse if(isMockMode()){...} and drop trailing return request
out = []
i = 0
collapsed = False
while i < len(sl):
if "if (isMockMode())" in sl[i]:
ci = find_close(sl, i)
if ci is not None:
inner = sl[i+1:ci]
inner_out = []
for il in inner:
if "return request" in il:
continue
if il.lstrip().startswith("//") and "【待确认】" in il:
continue
inner_out.append(il)
inner_out = [l[2:] if l.startswith(" ") else l for l in inner_out]
out.append(" // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支")
out.extend(inner_out)
i = ci + 1
collapsed = True
continue
if "return request" in sl[i]:
i += 1
continue
out.append(strip_dai(sl[i]))
i += 1
if not collapsed:
# dai but no isMockMode block and no real branch -> just strip
changed["strip_only"].append(name)
else:
changed["revert"].append(name)
new_full.extend(out)
# preserve trailing lines after the last function
new_full.extend(lines[func_ranges[-1][2]:])
with open("src/api/api.ts", "w", encoding="utf-8") as f:
f.write("\n".join(new_full))
print("PATHFIX:", len(changed["pathfix"]))
print("REVERT:", len(changed["revert"]))
print("STRIP_ONLY:", len(changed["strip_only"]), changed["strip_only"])
print("--- revert ---")
for n in changed["revert"]:
print(" ", n)

249
src/__tests__/backend-bridge-remaining.test.ts

@ -0,0 +1,249 @@
/**
* 后端桥接(剩余域)consolidated 测试
* 覆盖第三轮之后批量转换的 62 个 fetch 函数:
* - 真实态(setMockMode(false)):仅对已与后端 OpenAPI 对齐的函数断言 request 命中真实路径
* (见 REAL_FUNCS;其余为幽灵域,已退回 mock-only,翻真实开关也不触网,不在此断言)
* - mock 态(默认):断言返回非 undefined(防止 mock 分支被意外破坏)
* 路径依据 scripts/openapi.json(job.youthome.com.cn)实测校正,不再标【待确认】。
*/
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } }));
// token-store:内存双 Token,避免依赖 AsyncStorage
jest.mock('../lib/token-store', () => {
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null;
return {
getAccessToken: jest.fn(() => mem?.accessToken ?? null),
getTokens: jest.fn(() => mem),
setTokens: jest.fn((a: string, r: string, e?: number) => {
mem = { accessToken: a, refreshToken: r, expiresAt: e };
}),
clearTokens: jest.fn(() => {
mem = null;
}),
hydrateTokens: jest.fn(async () => undefined),
};
});
// storage:内存实现,避免依赖 AsyncStorage
jest.mock('../lib/storage', () => {
const m = new Map<string, string>();
return {
storageGet: jest.fn(async (k: string) => m.get(k) ?? null),
storageSet: jest.fn(async (k: string, v: string) => {
m.set(k, v);
}),
storageRemove: jest.fn(async (k: string) => {
m.delete(k);
}),
};
});
import {
fetchArticles,
fetchMenuItems,
fetchEnterpriseEntry,
fetchRankData,
fetchSkillCloud,
fetchSkillLearningPath,
fetchSalaryToolLinks,
fetchSalaryBrackets,
fetchOperationRules,
fetchHollandIntro,
fetchIdentityOptions,
fetchGameLevels,
fetchLegalBrand,
fetchLegalServices,
fetchLegalFAQs,
fetchLegalTemplates,
fetchLegalPlans,
fetchAboutInfo,
fetchQualifications,
fetchAITools,
fetchHistory,
fetchHollandAssessment,
fetchHollandResult,
fetchAbilityAssessment,
fetchUserStats,
fetchOverallScore,
fetchDimensionScores,
fetchBadges,
fetchOfferCompareData,
fetchPendingOfferCount,
fetchWeightSettings,
fetchSalaryRecords,
fetchAIInterviewData,
fetchHREvaluation,
fetchAppealForm,
fetchEnterpriseOverview,
fetchJobPublishForm,
fetchJobEvaluationManagement,
fetchCandidateDetail,
fetchInterviewScore,
fetchInterviewScoreDraft,
fetchAuditReport,
fetchEnterpriseCertification,
fetchJobManagement,
fetchTalentRecommendation,
fetchEnterpriseCommunication,
fetchJobEvaluationEmptyState,
fetchGameProgress,
fetchLegalMembership,
fetchLegalOrders,
fetchLegalServiceRequests,
fetchLegalConversation,
fetchPrivacySettings,
fetchPrivacyCenter,
fetchPrivacyDetailSetting,
fetchDndSetting,
fetchKeywordBlock,
fetchAccountCancel,
fetchLegalDocument,
fetchPersonalDataExport,
fetchNotificationSettings,
fetchAccountSecurity,
} from '../api/api';
import { setMockMode } from '../api/http';
// [fn, args, expectedPath, mockSmoke]
type Entry = [
(...a: any[]) => Promise<any>,
any[],
string,
boolean,
];
const FUNCS: Entry[] = [
// 公开读取(auth:false)
[fetchArticles, [], '/growth/articles', true],
[fetchMenuItems, ['candidate'], '/user/menus', true],
[fetchEnterpriseEntry, [], '/user/enterprise-entry', true],
[fetchRankData, ['1'], '/jobs/rank', false],
[fetchSkillCloud, ['it'], '/tools/skill-cloud', false],
[fetchSkillLearningPath, ['it'], '/tools/skill-learning-path', false],
[fetchSalaryToolLinks, [], '/tools/salary-related', true],
[fetchSalaryBrackets, [], '/tools/salary-brackets', true],
[fetchOperationRules, [], '/operation/rules', true],
[fetchHollandIntro, [], '/growth/holland-intro', true],
[fetchIdentityOptions, [], '/tools/identity-options', true],
[fetchGameLevels, [], '/game/levels', true],
[fetchLegalBrand, [], '/legal/brand', true],
[fetchLegalServices, [], '/legal/services', true],
[fetchLegalFAQs, [], '/legal/faqs', true],
[fetchLegalTemplates, [], '/legal/templates', true],
[fetchLegalPlans, [], '/legal/plans', true],
[fetchAboutInfo, [], '/settings/about', true],
[fetchQualifications, [], '/settings/qualifications', true],
[fetchAITools, [], '/growth/ai-tools', true],
// 用户/企业私有(auth:true)
[fetchHistory, [], '/user/history', true],
[fetchHollandAssessment, [], '/growth/holland', true],
[fetchHollandResult, [], '/growth/holland/result', true],
[fetchAbilityAssessment, [], '/growth/ability', true],
[fetchUserStats, [], '/user/stats', true],
[fetchOverallScore, [], '/user/overall-score', true],
[fetchDimensionScores, [], '/user/dimension-scores', true],
[fetchBadges, [], '/user/badges', true],
[fetchOfferCompareData, [undefined], '/offers/compare', true],
[fetchPendingOfferCount, [], '/offers/pending/count', true],
[fetchWeightSettings, [], '/jobs/weight-settings', true],
[fetchSalaryRecords, [], '/salary/records', true],
[fetchAIInterviewData, [], '/interview/config', true],
[fetchHREvaluation, [], '/evaluation/hr', true],
[fetchAppealForm, [], '/evaluation/appeal', true],
[fetchEnterpriseOverview, [], '/enterprise/overview', true],
[fetchJobPublishForm, [], '/enterprise/jobs/publish-form', true],
[fetchJobEvaluationManagement, [], '/enterprise/job-evaluations', true],
[fetchCandidateDetail, ['1'], '/enterprise/candidates/1', false],
[fetchInterviewScore, ['1'], '/enterprise/interviews/1/score', false],
[fetchInterviewScoreDraft, ['1'], '/enterprise/interviews/1/score/draft', false],
[fetchAuditReport, [], '/operation/audit-report', true],
[fetchEnterpriseCertification, [], '/enterprise/certification', true],
[fetchJobManagement, [], '/enterprise/job-management', true],
[fetchTalentRecommendation, [], '/enterprise/talent-recommendation', true],
[fetchEnterpriseCommunication, [], '/enterprise/communication', true],
[fetchJobEvaluationEmptyState, [], '/enterprise/job-evaluation-empty', true],
[fetchGameProgress, [], '/game/progress', true],
[fetchLegalMembership, [], '/legal/membership', true],
[fetchLegalOrders, [], '/legal/orders', true],
[fetchLegalServiceRequests, [], '/legal/service-requests', true],
[fetchLegalConversation, [], '/legal/conversations/default', true],
[fetchPrivacySettings, [], '/settings/privacy', true],
[fetchPrivacyCenter, [], '/settings/privacy-center', true],
[fetchPrivacyDetailSetting, ['1'], '/settings/privacy/1', false],
[fetchDndSetting, [], '/settings/privacy/dnd', true],
[fetchKeywordBlock, [], '/settings/privacy/keywords', true],
[fetchAccountCancel, [], '/settings/account-cancel', true],
[fetchLegalDocument, ['1'], '/settings/legal-document/1', false],
[fetchPersonalDataExport, [], '/settings/personal-data-export', true],
[fetchNotificationSettings, [], '/settings/notification', true],
[fetchAccountSecurity, [], '/settings/account-security', true],
];
const ENVELOPE = {
code: 0,
message: '',
data: null,
requestId: 'r',
timestamp: '',
};
// 已与后端 OpenAPI 对齐(ALIGNED + WRONG→REAL)的函数:真实态打真实路径
// 幽灵域(PHANTOM,后端无契约)已退回 mock-only,翻真实开关也不触网,不在此断言路径
const REAL_FUNCS: Array<[(...a: any[]) => Promise<any>, any[], string]> = [
[fetchRankData, ['1'], '/jobs/1/competition-ranking'],
[fetchCandidateDetail, ['1'], '/company/candidates/1'],
[fetchInterviewScore, ['1'], '/interviews/1/score'],
[fetchInterviewScoreDraft, ['1'], '/interviews/1/score'],
[fetchOfferCompareData, [undefined], '/offers/compare'],
];
describe('后端桥接 - 真实态路径断言(已对齐函数)', () => {
const originalFetch = (global as any).fetch;
beforeEach(() => {
(global as any).fetch = jest.fn(async () => ({
ok: true,
status: 200,
headers: { get: () => null },
json: async () => ENVELOPE,
text: async () => '',
}));
setMockMode(false);
});
afterEach(() => {
setMockMode(true);
});
afterAll(() => {
(global as any).fetch = originalFetch;
});
it('已对齐函数数量正确(5 个)', () => {
expect(REAL_FUNCS.length).toBe(5);
});
REAL_FUNCS.forEach(([fn, args, path], i) => {
it(`#${i} 命中真实路径 ${path}`, async () => {
await fn(...args);
const calls = ((global as any).fetch as jest.Mock).mock.calls;
expect(calls.length).toBeGreaterThan(0);
const calledUrl: string = calls[calls.length - 1][0];
expect(calledUrl).toContain(path);
});
});
});
describe('后端桥接 - mock 态冒烟(无回归)', () => {
beforeEach(() => {
setMockMode(true);
});
FUNCS.filter((e) => e[3]).forEach(([fn, args, path], idx) => {
it(`mock 态 ${path} 返回非 undefined`, async () => {
const result = await fn(...args);
expect(result).not.toBeUndefined();
});
});
});

141
src/__tests__/backend-bridge-write.test.ts

@ -0,0 +1,141 @@
/**
* 后端桥接(写入类)测试
* 覆盖本轮转换的 12 个写函数(简历 8 + 求职偏好 + 权重 + 薪资 2):
* - 真实态(setMockMode(false)):断言 request 命中预期方法 + 相对路径 + body
* - mock 态(默认):断言返回本地派生/生成值(证明 mock 分支未被破坏,本地 storage 真写入仍可用)
* 路径依据 scripts/openapi.json(job.youthome.com.cn)实测校正,不再标【待确认】。
* 幽灵域写函数已退回 mock-only,真实态不在此断言网络路径。
*/
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } }));
// token-store:内存双 Token,避免依赖 AsyncStorage
jest.mock('../lib/token-store', () => {
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null;
return {
getAccessToken: jest.fn(() => mem?.accessToken ?? null),
getTokens: jest.fn(() => mem),
setTokens: jest.fn((a: string, r: string, e?: number) => {
mem = { accessToken: a, refreshToken: r, expiresAt: e };
}),
clearTokens: jest.fn(() => {
mem = null;
}),
hydrateTokens: jest.fn(async () => undefined),
};
});
// storage:内存实现,避免依赖 AsyncStorage
jest.mock('../lib/storage', () => {
const m = new Map<string, string>();
return {
storageGet: jest.fn(async (k: string) => m.get(k) ?? null),
storageSet: jest.fn(async (k: string, v: string) => {
m.set(k, v);
}),
storageRemove: jest.fn(async (k: string) => {
m.delete(k);
}),
};
});
import { setMockMode } from '../api/http';
import {
saveResumeBasicInfo,
saveResumeSelfEvaluation,
saveResumeSkills,
saveResumeEducation,
saveResumeExperience,
saveResumeProjects,
saveResumeSnapshot,
saveResumeExportSettings,
saveJobPreference,
saveWeightSettings,
saveSalaryRecord,
deleteSalaryRecord,
} from '../api/api';
const ENVELOPE = { code: 0, message: 'ok', data: null, requestId: 'r', timestamp: 0 };
let fetchMock: jest.Mock;
beforeEach(() => {
fetchMock = jest.fn(async () => ({
ok: true,
status: 200,
json: async () => ENVELOPE,
text: async () => '',
headers: { get: () => 'x' },
}));
(global as any).fetch = fetchMock;
setMockMode(false); // 默认跑真实态
});
/** 触发一次真实请求,返回最后一个 fetch 调用的 options */
async function expectRequest(
call: () => Promise<unknown>,
path: string,
method: string,
): Promise<any> {
await call();
const last = fetchMock.mock.calls[fetchMock.mock.calls.length - 1];
expect(last[0]).toContain(path);
expect((last[1] as any).method).toBe(method);
return last[1] as any;
}
describe('写入函数真实态:方法 + 相对路径 + body(仅已对齐函数)', () => {
beforeEach(() => setMockMode(false));
test('saveJobPreference → PUT /profile/me/preferences', async () => {
const preference = { city: '北京', salary: '15k' } as any;
const opts = await expectRequest(
() => saveJobPreference(preference),
'/profile/me/preferences',
'PUT',
);
expect(JSON.parse(opts.body)).toEqual(preference);
});
});
/**
* 以下写函数属幽灵域(后端无 OpenAPI 契约),已退回 mock-only,翻真实开关也不触网,
* 故真实态不再断言网络路径;其 mock 态本地派生/生成行为见下方 describe。
*/
describe('写入函数 mock 态:本地派生/生成行为不变(开发期真写入仍可用)', () => {
beforeEach(() => setMockMode(true));
test('saveSalaryRecord 本地生成 id 与时间戳', async () => {
const rec = await saveSalaryRecord({} as any, {} as any, 'note');
expect(rec.id.startsWith('salary-')).toBe(true);
expect(typeof rec.createdAt).toBe('number');
expect(rec.note).toBe('note');
});
test('saveResumeBasicInfo 本地派生 avatar', async () => {
const info = await saveResumeBasicInfo({ name: '张三' } as any);
expect(info.avatar).toBeTruthy();
});
test('saveJobPreference 原样返回入参', async () => {
const p = { city: '北京' } as any;
expect(await saveJobPreference(p)).toBe(p);
});
test('saveResumeSkills 本地去空去重', async () => {
const out = await saveResumeSkills([
{ name: 'a' },
{ name: 'a' },
{ name: ' ' },
] as any);
expect(out).toEqual([{ name: 'a' }]);
});
test('saveWeightSettings 返回副本(不与入参同引用)', async () => {
const dims = [{ id: 'd1' }] as any;
const out = await saveWeightSettings(dims);
expect(out).not.toBe(dims);
expect(out).toEqual(dims);
});
});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save