Browse Source

feat(theme): 新增间距设计令牌层并收敛 onboarding 非卡片间距

- 新增 src/theme/spacing.ts:标尺(xs4/sm8/md12/lg16/xl24) + 语义令牌(card8/gutter8/framework16/block12/section12),src/theme/index.ts 从 ./spacing 导出
- 扩展 scripts/check-spacing.mjs:resolveSpacing 解析 spacing./spacingTokens. 引用,令牌化卡片不再被静默跳过(修潜在 bug),输出新增令牌命中统计
- onboarding 35 文件非卡片间距令牌化+收敛(18→block/22→section/14→block/10→sm/12→block)
- 更新项目开发状态.md 4.21
- B5(app 框架 padding:16) 本批延期,spacing.framework 单一来源已就绪
master
doublex001 1 month ago
parent
commit
928c310c36
  1. 6
      components/onboarding/screens/B1Wechat.tsx
  2. 6
      components/onboarding/screens/B2Agreement.tsx
  3. 8
      components/onboarding/screens/B3Recruiter.tsx
  4. 6
      components/onboarding/screens/P10FirstWork.tsx
  5. 8
      components/onboarding/screens/P11CurrentPosition.tsx
  6. 6
      components/onboarding/screens/P13JoinDate.tsx
  7. 8
      components/onboarding/screens/P14Skills.tsx
  8. 6
      components/onboarding/screens/P16WorkContent.tsx
  9. 6
      components/onboarding/screens/P17Reference.tsx
  10. 6
      components/onboarding/screens/P18Education.tsx
  11. 8
      components/onboarding/screens/P1Login.tsx
  12. 8
      components/onboarding/screens/P21Avatar.tsx
  13. 4
      components/onboarding/screens/P22Done.tsx
  14. 14
      components/onboarding/screens/P2PasswordLogin.tsx
  15. 14
      components/onboarding/screens/P2PasswordReset.tsx
  16. 12
      components/onboarding/screens/P2PhoneCode.tsx
  17. 10
      components/onboarding/screens/P2QuickLogin.tsx
  18. 14
      components/onboarding/screens/P2WechatCode.tsx
  19. 6
      components/onboarding/screens/P3Identity.tsx
  20. 6
      components/onboarding/screens/P4Intent.tsx
  21. 4
      components/onboarding/screens/P5City.tsx
  22. 12
      components/onboarding/screens/P6ExpectedPosition.tsx
  23. 4
      components/onboarding/screens/P7Salary.tsx
  24. 6
      components/onboarding/screens/P8Resume1.tsx
  25. 8
      components/onboarding/screens/P9JobStatus.tsx
  26. 4
      components/onboarding/ui/AgreementCheckbox.tsx
  27. 4
      components/onboarding/ui/AgreementModal.tsx
  28. 4
      components/onboarding/ui/BrandHeader.tsx
  29. 4
      components/onboarding/ui/DeletedAccountModal.tsx
  30. 8
      components/onboarding/ui/Modal.tsx
  31. 8
      components/onboarding/ui/ScreenShell.tsx
  32. 4
      components/onboarding/ui/SliderCaptcha.tsx
  33. 4
      components/onboarding/ui/SupportFooter.tsx
  34. 4
      components/onboarding/ui/Tag.tsx
  35. 6
      components/onboarding/ui/WheelPicker.tsx
  36. 57
      scripts/check-spacing.mjs
  37. 5
      src/theme/index.ts
  38. 42
      src/theme/spacing.ts
  39. 36
      项目开发状态.md

6
components/onboarding/screens/B1Wechat.tsx

@ -1,5 +1,5 @@
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Illustration } from '../ui/Illustration'; import { Illustration } from '../ui/Illustration';
@ -31,8 +31,8 @@ export function B1Wechat() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 18, paddingBottom: 40 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing.block, paddingBottom: 40 },
preview: { flexDirection: 'row', alignItems: 'center', gap: 12, backgroundColor: colors.white, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: colors.border }, preview: { flexDirection: 'row', alignItems: 'center', gap: spacing.block, backgroundColor: colors.white, padding: 8, borderRadius: 8, borderWidth: 1, borderColor: colors.border },
previewText: { gap: 4 }, previewText: { gap: 4 },
nick: { fontSize: 15, fontWeight: '700', color: colors.textPrimary }, nick: { fontSize: 15, fontWeight: '700', color: colors.textPrimary },
tip: { fontSize: 12, color: colors.textHint }, tip: { fontSize: 12, color: colors.textHint },

6
components/onboarding/screens/B2Agreement.tsx

@ -1,5 +1,5 @@
import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
@ -33,6 +33,6 @@ export function B2Agreement() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
body: { flex: 1 }, body: { flex: 1 },
h: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, marginBottom: 14 }, h: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, marginBottom: spacing.block },
p: { fontSize: 14, color: colors.textSecondary, lineHeight: 24, marginBottom: 14 }, p: { fontSize: 14, color: colors.textSecondary, lineHeight: 24, marginBottom: spacing.block },
}); });

8
components/onboarding/screens/B3Recruiter.tsx

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Image, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Image, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { import {
useEnterpriseCertification, useEnterpriseCertification,
useImagePick, useImagePick,
@ -111,7 +111,7 @@ export function B3Recruiter() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
tip: { fontSize: 13, color: colors.textHint, marginBottom: 18 }, tip: { fontSize: 13, color: colors.textHint, marginBottom: spacing.block },
label: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginTop: 16, marginBottom: 8 }, label: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginTop: 16, marginBottom: 8 },
input: { input: {
height: 50, height: 50,
@ -127,7 +127,7 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 8, gap: 8,
paddingVertical: 18, paddingVertical: spacing.block,
borderRadius: 12, borderRadius: 12,
borderWidth: 1, borderWidth: 1,
borderStyle: 'dashed', borderStyle: 'dashed',
@ -137,6 +137,6 @@ const styles = StyleSheet.create({
dim: { opacity: 0.5 }, dim: { opacity: 0.5 },
licensePreview: { width: 160, height: 120, borderRadius: 12 }, licensePreview: { width: 160, height: 120, borderRadius: 12 },
uploadText: { fontSize: 13, color: colors.textHint }, uploadText: { fontSize: 13, color: colors.textHint },
error: { fontSize: 12, color: colors.dangerText, marginTop: 10 }, error: { fontSize: 12, color: colors.dangerText, marginTop: spacing.sm },
footer: { marginTop: 28 }, footer: { marginTop: 28 },
}); });

6
components/onboarding/screens/P10FirstWork.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { YearMonthWheel } from '../ui/WheelPicker'; import { YearMonthWheel } from '../ui/WheelPicker';
@ -42,8 +42,8 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center', alignSelf: 'center',
marginBottom: 12, marginBottom: spacing.block,
}, },
tip: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginBottom: 18 }, tip: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginBottom: spacing.block },
footer: { marginTop: 28 }, footer: { marginTop: 28 },
}); });

8
components/onboarding/screens/P11CurrentPosition.tsx

@ -1,6 +1,6 @@
import { useState } from 'react'; import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { SearchInput } from '../ui/SearchInput'; import { SearchInput } from '../ui/SearchInput';
@ -85,14 +85,14 @@ export function P11CurrentPosition() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
body: { flexDirection: 'row', gap: 14, marginTop: 8 }, body: { flexDirection: 'row', gap: spacing.block, marginTop: 8 },
cats: { width: 92 }, cats: { width: 92 },
catItem: { paddingVertical: 12, borderRadius: 10 }, catItem: { paddingVertical: spacing.block, borderRadius: 10 },
catItemActive: { backgroundColor: colors.primaryLight }, catItemActive: { backgroundColor: colors.primaryLight },
catText: { fontSize: 13, color: colors.textSecondary, textAlign: 'center' }, catText: { fontSize: 13, color: colors.textSecondary, textAlign: 'center' },
catTextActive: { color: colors.primaryText, fontWeight: '700' }, catTextActive: { color: colors.primaryText, fontWeight: '700' },
col: { flex: 1 }, col: { flex: 1 },
colTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: 12 }, colTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: spacing.block },
empty: { fontSize: 13, color: colors.textHint, lineHeight: 20 }, empty: { fontSize: 13, color: colors.textHint, lineHeight: 20 },
blockTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginVertical: 16 }, blockTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginVertical: 16 },
}); });

6
components/onboarding/screens/P13JoinDate.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { YearMonthWheel } from '../ui/WheelPicker'; import { YearMonthWheel } from '../ui/WheelPicker';
@ -47,8 +47,8 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center', alignSelf: 'center',
marginBottom: 12, marginBottom: spacing.block,
}, },
tip: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginBottom: 18 }, tip: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginBottom: spacing.block },
footer: { marginTop: 28 }, footer: { marginTop: 28 },
}); });

8
components/onboarding/screens/P14Skills.tsx

@ -1,5 +1,5 @@
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Tag, TagGroup } from '../ui/Tag'; import { Tag, TagGroup } from '../ui/Tag';
@ -66,13 +66,13 @@ export function P14Skills() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: spacing.block },
title: { fontSize: 20, fontWeight: '800', color: colors.textPrimary }, title: { fontSize: 20, fontWeight: '800', color: colors.textPrimary },
counter: { backgroundColor: colors.borderLight, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 12 }, counter: { backgroundColor: colors.borderLight, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 12 },
counterActive: { backgroundColor: colors.primaryLight }, counterActive: { backgroundColor: colors.primaryLight },
counterText: { fontSize: 13, fontWeight: '700', color: colors.textDisabled }, counterText: { fontSize: 13, fontWeight: '700', color: colors.textDisabled },
counterTextActive: { color: colors.primaryText }, counterTextActive: { color: colors.primaryText },
group: { marginBottom: 18 }, group: { marginBottom: spacing.block },
groupName: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: 10 }, groupName: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: spacing.sm },
footer: { marginTop: 20 }, footer: { marginTop: 20 },
}); });

6
components/onboarding/screens/P16WorkContent.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding, useRegisterFinish } from '../../../src/hooks'; import { useOnboarding, useRegisterFinish } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { PrimaryButton } from '../ui/PrimaryButton'; import { PrimaryButton } from '../ui/PrimaryButton';
@ -45,10 +45,10 @@ export function P16WorkContent() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: spacing.block },
title: { fontSize: 20, fontWeight: '800', color: colors.textPrimary }, title: { fontSize: 20, fontWeight: '800', color: colors.textPrimary },
save: { fontSize: 15, fontWeight: '700', color: colors.primary }, save: { fontSize: 15, fontWeight: '700', color: colors.primary },
ref: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', marginBottom: 14 }, ref: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', marginBottom: spacing.block },
refText: { fontSize: 14, color: colors.primary, fontWeight: '600' }, refText: { fontSize: 14, color: colors.primary, fontWeight: '600' },
input: { input: {
minHeight: 200, minHeight: 200,

6
components/onboarding/screens/P17Reference.tsx

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Avatar } from '../ui/Avatar'; import { Avatar } from '../ui/Avatar';
@ -73,7 +73,7 @@ export function P17Reference() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
head: { flexDirection: 'row', alignItems: 'center', gap: 14, marginBottom: 18 }, head: { flexDirection: 'row', alignItems: 'center', gap: spacing.block, marginBottom: spacing.block },
headText: { gap: 8 }, headText: { gap: 8 },
name: { fontSize: 16, fontWeight: '700', color: colors.textPrimary }, name: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
swap: { flexDirection: 'row', alignItems: 'center', gap: 4 }, swap: { flexDirection: 'row', alignItems: 'center', gap: 4 },
@ -82,7 +82,7 @@ const styles = StyleSheet.create({
backgroundColor: colors.white, backgroundColor: colors.white,
borderRadius: 8, borderRadius: 8,
padding: 8, padding: 8,
gap: 12, gap: spacing.block,
borderWidth: 1, borderWidth: 1,
borderColor: colors.border, borderColor: colors.border,
}, },

6
components/onboarding/screens/P18Education.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { RadioRow } from '../ui/RadioRow'; import { RadioRow } from '../ui/RadioRow';
@ -48,9 +48,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center', alignSelf: 'center',
marginBottom: 18, marginBottom: spacing.block,
}, },
list: { gap: 8 }, list: { gap: 8 },
subTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginTop: 22, marginBottom: 12 }, subTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginTop: spacing.section, marginBottom: spacing.block },
footer: { marginTop: 28 }, footer: { marginTop: 28 },
}); });

8
components/onboarding/screens/P1Login.tsx

@ -2,7 +2,7 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useAgreementGate, useOnboarding } from '../../../src/hooks'; import { useAgreementGate, useOnboarding } from '../../../src/hooks';
import { Illustration } from '../ui/Illustration'; import { Illustration } from '../ui/Illustration';
import { AgreementCheckbox } from '../ui/AgreementCheckbox'; import { AgreementCheckbox } from '../ui/AgreementCheckbox';
@ -76,7 +76,7 @@ export function P1Login() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: colors.pageBg }, screen: { flex: 1, backgroundColor: colors.pageBg },
topGlow: { position: 'absolute', top: 0, left: 0, right: 0, height: 200 }, topGlow: { position: 'absolute', top: 0, left: 0, right: 0, height: 200 },
content: { flexGrow: 1, paddingHorizontal: 8, paddingBottom: 12 }, content: { flexGrow: 1, paddingHorizontal: 8, paddingBottom: spacing.block },
brandArea: { alignItems: 'center', marginTop: '22%' }, brandArea: { alignItems: 'center', marginTop: '22%' },
flexSpacer: { flex: 1, minHeight: 28 }, flexSpacer: { flex: 1, minHeight: 28 },
quickBtn: { quickBtn: {
@ -84,7 +84,7 @@ const styles = StyleSheet.create({
borderRadius: 12, borderRadius: 12,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginTop: 14, marginTop: spacing.block,
backgroundColor: colors.borderLight, backgroundColor: colors.borderLight,
}, },
quickText: { fontSize: 16, fontWeight: '700', color: colors.textPrimary }, quickText: { fontSize: 16, fontWeight: '700', color: colors.textPrimary },
@ -94,7 +94,7 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
color: colors.textSecondary, color: colors.textSecondary,
}, },
agree: { marginTop: 22 }, agree: { marginTop: spacing.section },
agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 }, agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
link: { color: colors.primary, fontWeight: '600' }, link: { color: colors.primary, fontWeight: '600' },
footer: { paddingTop: 24, paddingBottom: 8 }, footer: { paddingTop: 24, paddingBottom: 8 },

8
components/onboarding/screens/P21Avatar.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useImagePick, useOnboarding } from '../../../src/hooks'; import { useImagePick, useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Avatar } from '../ui/Avatar'; import { Avatar } from '../ui/Avatar';
@ -84,8 +84,8 @@ const styles = StyleSheet.create({
actionBtn: { alignItems: 'center', gap: 6 }, actionBtn: { alignItems: 'center', gap: 6 },
actionBtnDim: { opacity: 0.5 }, actionBtnDim: { opacity: 0.5 },
actionText: { fontSize: 12, color: colors.textSecondary }, actionText: { fontSize: 12, color: colors.textSecondary },
error: { fontSize: 12, color: colors.dangerText, textAlign: 'center', marginBottom: 18 }, error: { fontSize: 12, color: colors.dangerText, textAlign: 'center', marginBottom: spacing.block },
subTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: 14 }, subTitle: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginBottom: spacing.block },
presets: { flexDirection: 'row', justifyContent: 'space-between', gap: 10 }, presets: { flexDirection: 'row', justifyContent: 'space-between', gap: spacing.sm },
footer: { marginTop: 32 }, footer: { marginTop: 32 },
}); });

4
components/onboarding/screens/P22Done.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useRegisterFinish } from '../../../src/hooks'; import { useRegisterFinish } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Illustration } from '../ui/Illustration'; import { Illustration } from '../ui/Illustration';
@ -28,7 +28,7 @@ export function P22Done() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 22, paddingBottom: 40 }, center: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: spacing.section, paddingBottom: 40 },
glow: { glow: {
width: 120, width: 120,
height: 120, height: 120,

14
components/onboarding/screens/P2PasswordLogin.tsx

@ -2,7 +2,7 @@ import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useAgreementGate, useOnboarding, usePasswordLogin } from '../../../src/hooks'; import { useAgreementGate, useOnboarding, usePasswordLogin } from '../../../src/hooks';
import { isValidPhone } from '../../../src/utils'; import { isValidPhone } from '../../../src/utils';
import { useAuth } from '../../../src/context/AuthContext'; import { useAuth } from '../../../src/context/AuthContext';
@ -121,18 +121,18 @@ export function P2PasswordLogin() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: 12 }, h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: spacing.block },
subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: 10 }, subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: spacing.sm },
fieldRow: { flexDirection: 'row', alignItems: 'center', height: 56, marginTop: 28, gap: 6 }, fieldRow: { flexDirection: 'row', alignItems: 'center', height: 56, marginTop: 28, gap: 6 },
prefix: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, prefix: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
divider: { width: 1, height: 20, backgroundColor: colors.border, marginHorizontal: 10 }, divider: { width: 1, height: 20, backgroundColor: colors.border, marginHorizontal: 10 },
input: { flex: 1, fontSize: 17, color: colors.textPrimary, padding: 0 }, input: { flex: 1, fontSize: 17, color: colors.textPrimary, padding: 0 },
underline: { height: 1, backgroundColor: colors.borderLight }, underline: { height: 1, backgroundColor: colors.borderLight },
passwordArea: { marginTop: 22 }, passwordArea: { marginTop: spacing.section },
forgotLink: { marginTop: 14, alignSelf: 'flex-end', fontSize: 13, color: colors.primaryText, fontWeight: '600' }, forgotLink: { marginTop: spacing.block, alignSelf: 'flex-end', fontSize: 13, color: colors.primaryText, fontWeight: '600' },
btnArea: { marginTop: 26 }, btnArea: { marginTop: 26 },
formError: { marginTop: 10, textAlign: 'center', fontSize: 12, color: colors.danger }, formError: { marginTop: spacing.sm, textAlign: 'center', fontSize: 12, color: colors.danger },
agree: { marginTop: 18 }, agree: { marginTop: spacing.block },
agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 }, agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
link: { color: colors.primary, fontWeight: '600' }, link: { color: colors.primary, fontWeight: '600' },
switchLink: { marginTop: 26, textAlign: 'center', fontSize: 14, color: colors.textSecondary }, switchLink: { marginTop: 26, textAlign: 'center', fontSize: 14, color: colors.textSecondary },

14
components/onboarding/screens/P2PasswordReset.tsx

@ -2,7 +2,7 @@ import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useAgreementGate, useOnboarding, usePasswordReset, useSmsCode } from '../../../src/hooks'; import { useAgreementGate, useOnboarding, usePasswordReset, useSmsCode } from '../../../src/hooks';
import { isValidPhone, isValidSmsCode, SMS_CODE_MAX_LENGTH } from '../../../src/utils'; import { isValidPhone, isValidSmsCode, SMS_CODE_MAX_LENGTH } from '../../../src/utils';
import { useAuth } from '../../../src/context/AuthContext'; import { useAuth } from '../../../src/context/AuthContext';
@ -167,16 +167,16 @@ export function P2PasswordReset() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: 12 }, h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: spacing.block },
subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: 10 }, subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: spacing.sm },
fieldRow: { flexDirection: 'row', alignItems: 'center', height: 56, marginTop: 26, gap: 6 }, fieldRow: { flexDirection: 'row', alignItems: 'center', height: 56, marginTop: 26, gap: 6 },
prefix: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, prefix: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
divider: { width: 1, height: 20, backgroundColor: colors.border, marginHorizontal: 10 }, divider: { width: 1, height: 20, backgroundColor: colors.border, marginHorizontal: 10 },
input: { flex: 1, fontSize: 17, color: colors.textPrimary, padding: 0 }, input: { flex: 1, fontSize: 17, color: colors.textPrimary, padding: 0 },
underline: { height: 1, backgroundColor: colors.borderLight }, underline: { height: 1, backgroundColor: colors.borderLight },
passwordArea: { marginTop: 22 }, passwordArea: { marginTop: spacing.section },
hint: { marginTop: 14, fontSize: 12, color: colors.textHint }, hint: { marginTop: spacing.block, fontSize: 12, color: colors.textHint },
hintDanger: { marginTop: 14, fontSize: 12, color: colors.danger }, hintDanger: { marginTop: spacing.block, fontSize: 12, color: colors.danger },
btnArea: { marginTop: 26 }, btnArea: { marginTop: 26 },
formError: { marginTop: 10, textAlign: 'center', fontSize: 12, color: colors.danger }, formError: { marginTop: spacing.sm, textAlign: 'center', fontSize: 12, color: colors.danger },
}); });

12
components/onboarding/screens/P2PhoneCode.tsx

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Alert, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Alert, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { maskPhone } from '../../../src/api'; import { maskPhone } from '../../../src/api';
import { SMS_HELP_BODY, SMS_HELP_TITLE } from '../../../src/constants'; import { SMS_HELP_BODY, SMS_HELP_TITLE } from '../../../src/constants';
import { useAgreementGate, useLogin, useOnboarding, useSmsCode, useWechatBindPhone } from '../../../src/hooks'; import { useAgreementGate, useLogin, useOnboarding, useSmsCode, useWechatBindPhone } from '../../../src/hooks';
@ -215,8 +215,8 @@ export function P2PhoneCode() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: 12 }, h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: spacing.block },
subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: 10 }, subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: spacing.sm },
phoneRow: { phoneRow: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
@ -232,11 +232,11 @@ const styles = StyleSheet.create({
smsHelpRow: { alignSelf: 'flex-start', marginTop: 16 }, smsHelpRow: { alignSelf: 'flex-start', marginTop: 16 },
smsHelp: { fontSize: 13, color: colors.primaryText }, smsHelp: { fontSize: 13, color: colors.primaryText },
btnArea: { marginTop: 28 }, btnArea: { marginTop: 28 },
agree: { marginTop: 18 }, agree: { marginTop: spacing.block },
formError: { marginTop: 10, textAlign: 'center', fontSize: 12, color: colors.danger }, formError: { marginTop: spacing.sm, textAlign: 'center', fontSize: 12, color: colors.danger },
agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 }, agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
link: { color: colors.primary, fontWeight: '600' }, link: { color: colors.primary, fontWeight: '600' },
orRow: { flexDirection: 'row', alignItems: 'center', marginTop: 36, gap: 12 }, orRow: { flexDirection: 'row', alignItems: 'center', marginTop: 36, gap: spacing.block },
orLine: { flex: 1, height: 1, backgroundColor: colors.borderLight }, orLine: { flex: 1, height: 1, backgroundColor: colors.borderLight },
orText: { fontSize: 12, color: colors.textHint }, orText: { fontSize: 12, color: colors.textHint },
switchLink: { marginTop: 20, textAlign: 'center', fontSize: 14, color: colors.primaryText, fontWeight: '600' }, switchLink: { marginTop: 20, textAlign: 'center', fontSize: 14, color: colors.primaryText, fontWeight: '600' },

10
components/onboarding/screens/P2QuickLogin.tsx

@ -1,5 +1,5 @@
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { CARRIER_LABEL } from '../../../src/constants'; import { CARRIER_LABEL } from '../../../src/constants';
import { useAgreementGate, useCarrierPhone, useLogin, useOnboarding } from '../../../src/hooks'; import { useAgreementGate, useCarrierPhone, useLogin, useOnboarding } from '../../../src/hooks';
import { useAuth } from '../../../src/context/AuthContext'; import { useAuth } from '../../../src/context/AuthContext';
@ -154,20 +154,20 @@ const styles = StyleSheet.create({
marginTop: 44, marginTop: 44,
}, },
phoneFailed: { fontSize: 18, color: colors.dangerText }, phoneFailed: { fontSize: 18, color: colors.dangerText },
subtitle: { textAlign: 'center', fontSize: 14, color: colors.textSecondary, marginTop: 12 }, subtitle: { textAlign: 'center', fontSize: 14, color: colors.textSecondary, marginTop: spacing.block },
btnArea: { marginTop: 36 }, btnArea: { marginTop: 36 },
fallbackRow: { fallbackRow: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
gap: 24, gap: 24,
marginTop: 18, marginTop: spacing.block,
}, },
fallbackBtn: { paddingVertical: 6, paddingHorizontal: 4 }, fallbackBtn: { paddingVertical: 6, paddingHorizontal: 4 },
fallbackText: { fontSize: 14, fontWeight: '600', color: colors.primaryText }, fallbackText: { fontSize: 14, fontWeight: '600', color: colors.primaryText },
formError: { marginTop: 10, textAlign: 'center', fontSize: 12, color: colors.danger }, formError: { marginTop: spacing.sm, textAlign: 'center', fontSize: 12, color: colors.danger },
agree: { marginTop: 20 }, agree: { marginTop: 20 },
agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 }, agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
link: { color: colors.primary, fontWeight: '600' }, link: { color: colors.primary, fontWeight: '600' },
footer: { paddingTop: 16, paddingBottom: 8 }, footer: { paddingTop: 16, paddingBottom: 8 },
carrierNote: { textAlign: 'center', fontSize: 12, color: colors.textHint, marginTop: 12 }, carrierNote: { textAlign: 'center', fontSize: 12, color: colors.textHint, marginTop: spacing.block },
}); });

14
components/onboarding/screens/P2WechatCode.tsx

@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { Alert, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { Alert, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { SMS_HELP_BODY, SMS_HELP_TITLE } from '../../../src/constants'; import { SMS_HELP_BODY, SMS_HELP_TITLE } from '../../../src/constants';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
@ -109,10 +109,10 @@ export function P2WechatCode() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
h1: { fontSize: 24, fontWeight: '800', color: colors.textPrimary, marginTop: 14 }, h1: { fontSize: 24, fontWeight: '800', color: colors.textPrimary, marginTop: spacing.block },
subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: 12 }, subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: spacing.block },
cellsWrap: { marginTop: 34 }, cellsWrap: { marginTop: 34 },
cellsRow: { flexDirection: 'row', gap: 18 }, cellsRow: { flexDirection: 'row', gap: spacing.block },
cell: { flex: 1, alignItems: 'center' }, cell: { flex: 1, alignItems: 'center' },
cellText: { fontSize: 26, fontWeight: '700', color: colors.textPrimary, minHeight: 36 }, cellText: { fontSize: 26, fontWeight: '700', color: colors.textPrimary, minHeight: 36 },
cellLine: { width: '100%', height: 2, backgroundColor: colors.border, marginTop: 8 }, cellLine: { width: '100%', height: 2, backgroundColor: colors.border, marginTop: 8 },
@ -121,17 +121,17 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFill, ...StyleSheet.absoluteFill,
opacity: 0, opacity: 0,
}, },
resend: { textAlign: 'center', fontSize: 14, color: colors.textSecondary, marginTop: 22 }, resend: { textAlign: 'center', fontSize: 14, color: colors.textSecondary, marginTop: spacing.section },
resendDisabled: { color: colors.textHint }, resendDisabled: { color: colors.textHint },
btnArea: { marginTop: 30 }, btnArea: { marginTop: 30 },
helpRow: { alignSelf: 'flex-start', marginTop: 22 }, helpRow: { alignSelf: 'flex-start', marginTop: spacing.section },
// 已是可点入口,用可读对比度的 primaryText(textHint 在 13pt 下不达 AA) // 已是可点入口,用可读对比度的 primaryText(textHint 在 13pt 下不达 AA)
helpText: { fontSize: 13, color: colors.primaryText }, helpText: { fontSize: 13, color: colors.primaryText },
toastBar: { toastBar: {
alignSelf: 'center', alignSelf: 'center',
marginTop: 30, marginTop: 30,
paddingHorizontal: 18, paddingHorizontal: 18,
paddingVertical: 10, paddingVertical: spacing.sm,
borderRadius: 8, borderRadius: 8,
backgroundColor: colors.borderLight, backgroundColor: colors.borderLight,
}, },

6
components/onboarding/screens/P3Identity.tsx

@ -1,5 +1,5 @@
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native'; import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useAuth } from '../../../src/context/AuthContext'; import { useAuth } from '../../../src/context/AuthContext';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
@ -82,7 +82,7 @@ const styles = StyleSheet.create({
}, },
cardBtnOutline: { backgroundColor: colors.white, borderWidth: 1, borderColor: colors.primary }, cardBtnOutline: { backgroundColor: colors.white, borderWidth: 1, borderColor: colors.primary },
cardBtnText: { fontSize: 16, fontWeight: '700', color: colors.textInverse }, cardBtnText: { fontSize: 16, fontWeight: '700', color: colors.textInverse },
or: { textAlign: 'center', color: colors.textHint, marginVertical: 14, fontSize: 13 }, or: { textAlign: 'center', color: colors.textHint, marginVertical: spacing.block, fontSize: 13 },
logout: { marginTop: 22, alignItems: 'center' }, logout: { marginTop: spacing.section, alignItems: 'center' },
logoutText: { fontSize: 13, color: colors.textHint }, logoutText: { fontSize: 13, color: colors.textHint },
}); });

6
components/onboarding/screens/P4Intent.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Tag, TagGroup } from '../ui/Tag'; import { Tag, TagGroup } from '../ui/Tag';
@ -57,8 +57,8 @@ export function P4Intent() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginTop: 22, marginBottom: 12 }, section: { fontSize: 16, fontWeight: '700', color: colors.textPrimary, marginTop: spacing.section, marginBottom: spacing.block },
cityRow: { flexDirection: 'row', alignItems: 'center', gap: 10 }, cityRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
cityHint: { fontSize: 12, color: colors.textHint }, cityHint: { fontSize: 12, color: colors.textHint },
footer: { marginTop: 32 }, footer: { marginTop: 32 },
}); });

4
components/onboarding/screens/P5City.tsx

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { EXPECTED_CITY_OPTIONS, HOT_CITY_OPTIONS } from '../../../src/constants'; import { EXPECTED_CITY_OPTIONS, HOT_CITY_OPTIONS } from '../../../src/constants';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
@ -50,7 +50,7 @@ export function P5City() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
section: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginTop: 22, marginBottom: 12 }, section: { fontSize: 14, fontWeight: '700', color: colors.textPrimary, marginTop: spacing.section, marginBottom: spacing.block },
list: { gap: 2 }, list: { gap: 2 },
cityItem: { cityItem: {
flexDirection: 'row', flexDirection: 'row',

12
components/onboarding/screens/P6ExpectedPosition.tsx

@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding, useRegisterFinish } from '../../../src/hooks'; import { useOnboarding, useRegisterFinish } from '../../../src/hooks';
import { onboardingStepLabel } from '../../../src/utils'; import { onboardingStepLabel } from '../../../src/utils';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
@ -114,7 +114,7 @@ export function P6ExpectedPosition() {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10 }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: spacing.sm },
title: { fontSize: 20, fontWeight: '800', color: colors.textPrimary }, title: { fontSize: 20, fontWeight: '800', color: colors.textPrimary },
progress: { progress: {
alignSelf: 'flex-start', alignSelf: 'flex-start',
@ -122,19 +122,19 @@ const styles = StyleSheet.create({
paddingHorizontal: 10, paddingHorizontal: 10,
paddingVertical: 3, paddingVertical: 3,
borderRadius: 10, borderRadius: 10,
marginBottom: 12, marginBottom: spacing.block,
}, },
progressText: { fontSize: 12, fontWeight: '700', color: colors.primaryText }, progressText: { fontSize: 12, fontWeight: '700', color: colors.primaryText },
save: { fontSize: 15, fontWeight: '700', color: colors.primary }, save: { fontSize: 15, fontWeight: '700', color: colors.primary },
empty: { fontSize: 13, color: colors.textHint, lineHeight: 20 }, empty: { fontSize: 13, color: colors.textHint, lineHeight: 20 },
body: { flexDirection: 'row', gap: 14, marginTop: 8 }, body: { flexDirection: 'row', gap: spacing.block, marginTop: 8 },
cats: { width: 92 }, cats: { width: 92 },
catItem: { paddingVertical: 12, borderRadius: 10 }, catItem: { paddingVertical: spacing.block, borderRadius: 10 },
catItemActive: { backgroundColor: colors.primaryLight }, catItemActive: { backgroundColor: colors.primaryLight },
catText: { fontSize: 13, color: colors.textSecondary, textAlign: 'center' }, catText: { fontSize: 13, color: colors.textSecondary, textAlign: 'center' },
catTextActive: { color: colors.primaryText, fontWeight: '700' }, catTextActive: { color: colors.primaryText, fontWeight: '700' },
posCol: { flex: 1 }, posCol: { flex: 1 },
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 14 }, chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: spacing.block },
chip: { chip: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

4
components/onboarding/screens/P7Salary.tsx

@ -1,5 +1,5 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { applySalaryMin, salaryMaxOptions } from '../../../src/api'; import { applySalaryMin, salaryMaxOptions } from '../../../src/api';
import { SALARY_MIN_OPTIONS } from '../../../src/constants'; import { SALARY_MIN_OPTIONS } from '../../../src/constants';
@ -56,7 +56,7 @@ export function P7Salary() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
tip: { fontSize: 13, color: colors.textHint, marginBottom: 20 }, tip: { fontSize: 13, color: colors.textHint, marginBottom: 20 },
cols: { flexDirection: 'row', gap: 16 }, cols: { flexDirection: 'row', gap: 16 },
col: { flex: 1, gap: 12 }, col: { flex: 1, gap: spacing.block },
colTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, textAlign: 'center', marginBottom: 4 }, colTitle: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, textAlign: 'center', marginBottom: 4 },
opt: { opt: {
height: 50, height: 50,

6
components/onboarding/screens/P8Resume1.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, TextInput, View } from 'react-native'; import { StyleSheet, Text, TextInput, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
import { Tag, TagGroup } from '../ui/Tag'; import { Tag, TagGroup } from '../ui/Tag';
@ -64,9 +64,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center', alignSelf: 'center',
marginBottom: 18, marginBottom: spacing.block,
}, },
label: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginTop: 18, marginBottom: 10 }, label: { fontSize: 15, fontWeight: '700', color: colors.textPrimary, marginTop: spacing.block, marginBottom: spacing.sm },
input: { input: {
height: 50, height: 50,
borderRadius: 12, borderRadius: 12,

8
components/onboarding/screens/P9JobStatus.tsx

@ -1,6 +1,6 @@
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { useOnboarding } from '../../../src/hooks'; import { useOnboarding } from '../../../src/hooks';
import { JOB_STATUS_OPTIONS } from '../../../src/constants'; import { JOB_STATUS_OPTIONS } from '../../../src/constants';
import { ScreenShell } from '../ui/ScreenShell'; import { ScreenShell } from '../ui/ScreenShell';
@ -57,11 +57,11 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
alignSelf: 'center', alignSelf: 'center',
marginBottom: 18, marginBottom: spacing.block,
}, },
list: { gap: 8 }, list: { gap: 8 },
note: { fontSize: 12, color: colors.textHint, marginTop: 14, textAlign: 'center' }, note: { fontSize: 12, color: colors.textHint, marginTop: spacing.block, textAlign: 'center' },
agree: { marginTop: 18 }, agree: { marginTop: spacing.block },
agreeText: { fontSize: 13, color: colors.textSecondary }, agreeText: { fontSize: 13, color: colors.textSecondary },
footer: { marginTop: 28 }, footer: { marginTop: 28 },
}); });

4
components/onboarding/ui/AgreementCheckbox.tsx

@ -1,7 +1,7 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { Pressable, StyleSheet, View } from 'react-native'; import { Pressable, StyleSheet, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
interface Props { interface Props {
checked: boolean; checked: boolean;
@ -22,7 +22,7 @@ export function AgreementCheckbox({ checked, onToggle, text }: Props) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
row: { flexDirection: 'row', alignItems: 'center', gap: 10 }, row: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm },
box: { box: {
width: 20, width: 20,
height: 20, height: 20,

4
components/onboarding/ui/AgreementModal.tsx

@ -1,5 +1,5 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { Modal } from './Modal'; import { Modal } from './Modal';
interface Props { interface Props {
@ -38,7 +38,7 @@ export function AgreementModal({ visible, onAccept, onDecline, onOpenAgreement }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
message: { fontSize: 15, lineHeight: 24, color: colors.textPrimary, textAlign: 'center' }, message: { fontSize: 15, lineHeight: 24, color: colors.textPrimary, textAlign: 'center' },
link: { color: colors.primary }, link: { color: colors.primary },
btnRow: { flexDirection: 'row', gap: 14, marginTop: 20 }, btnRow: { flexDirection: 'row', gap: spacing.block, marginTop: 20 },
btn: { btn: {
flex: 1, flex: 1,
height: 46, height: 46,

4
components/onboarding/ui/BrandHeader.tsx

@ -1,5 +1,5 @@
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { Illustration } from './Illustration'; import { Illustration } from './Illustration';
/** 统一品牌头部:Logo + 名称 + 标语 */ /** 统一品牌头部:Logo + 名称 + 标语 */
@ -14,7 +14,7 @@ export function BrandHeader({ slogan }: { slogan?: string }) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
wrap: { alignItems: 'center', gap: 12, paddingVertical: 8 }, wrap: { alignItems: 'center', gap: spacing.block, paddingVertical: 8 },
name: { fontSize: 26, fontWeight: '800', color: colors.textPrimary }, name: { fontSize: 26, fontWeight: '800', color: colors.textPrimary },
slogan: { fontSize: 13, color: colors.textHint }, slogan: { fontSize: 13, color: colors.textHint },
}); });

4
components/onboarding/ui/DeletedAccountModal.tsx

@ -1,5 +1,5 @@
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { Modal } from './Modal'; import { Modal } from './Modal';
interface Props { interface Props {
@ -34,7 +34,7 @@ const styles = StyleSheet.create({
fontWeight: '600', fontWeight: '600',
color: colors.textPrimary, color: colors.textPrimary,
textAlign: 'center', textAlign: 'center',
marginBottom: 10, marginBottom: spacing.sm,
}, },
message: { fontSize: 14, lineHeight: 22, color: colors.textSecondary, textAlign: 'center' }, message: { fontSize: 14, lineHeight: 22, color: colors.textSecondary, textAlign: 'center' },
btnRow: { marginTop: 20 }, btnRow: { marginTop: 20 },

8
components/onboarding/ui/Modal.tsx

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
interface Props { interface Props {
visible: boolean; visible: boolean;
@ -35,7 +35,7 @@ const styles = StyleSheet.create({
padding: 8, padding: 8,
maxHeight: '80%', maxHeight: '80%',
}, },
title: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, marginBottom: 12, textAlign: 'center' }, title: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, marginBottom: spacing.block, textAlign: 'center' },
body: { gap: 10 }, body: { gap: spacing.sm },
footer: { marginTop: 18, gap: 10 }, footer: { marginTop: spacing.block, gap: spacing.sm },
}); });

8
components/onboarding/ui/ScreenShell.tsx

@ -3,7 +3,7 @@ import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
interface Props { interface Props {
title?: string; title?: string;
@ -43,10 +43,10 @@ export function ScreenShell({ title, showBack, onBack, backIcon = 'chevron-back'
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg }, container: { flex: 1, backgroundColor: colors.pageBg },
backRow: { flexDirection: 'row', alignItems: 'center', gap: 2, paddingHorizontal: 16, paddingVertical: 10 }, backRow: { flexDirection: 'row', alignItems: 'center', gap: 2, paddingHorizontal: 16, paddingVertical: spacing.sm },
titleInline: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, titleInline: { fontSize: 17, fontWeight: '700', color: colors.textPrimary },
titleCenter: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, textAlign: 'center', paddingVertical: 12 }, titleCenter: { fontSize: 18, fontWeight: '800', color: colors.textPrimary, textAlign: 'center', paddingVertical: spacing.block },
scroll: { flex: 1 }, scroll: { flex: 1 },
scrollContent: { flexGrow: 1, padding: 8, paddingBottom: 28 }, scrollContent: { flexGrow: 1, padding: 8, paddingBottom: 28 },
footer: { padding: 16, paddingBottom: 22, backgroundColor: colors.pageBg }, footer: { padding: 16, paddingBottom: spacing.section, backgroundColor: colors.pageBg },
}); });

4
components/onboarding/ui/SliderCaptcha.tsx

@ -14,7 +14,7 @@ import {
Stop, Stop,
Svg, Svg,
} from 'react-native-svg'; } from 'react-native-svg';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { Modal } from './Modal'; import { Modal } from './Modal';
import { CAPTCHA_BACKGROUNDS } from '../assets'; import { CAPTCHA_BACKGROUNDS } from '../assets';
@ -301,6 +301,6 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
alignItems: 'center', alignItems: 'center',
marginTop: 14, marginTop: spacing.block,
}, },
}); });

4
components/onboarding/ui/SupportFooter.tsx

@ -1,5 +1,5 @@
import { Linking, Pressable, StyleSheet, Text, View } from 'react-native'; import { Linking, Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { SUPERVISION_PHONE, SUPPORT_HOTLINE } from '../../../src/constants'; import { SUPERVISION_PHONE, SUPPORT_HOTLINE } from '../../../src/constants';
/** /**
@ -38,6 +38,6 @@ export function SupportFooter() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
// 两串「标签 + 11 位号码」在窄屏上会挤,允许换行 // 两串「标签 + 11 位号码」在窄屏上会挤,允许换行
row: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', gap: 18 }, row: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', gap: spacing.block },
item: { fontSize: 12, color: colors.textHint }, item: { fontSize: 12, color: colors.textHint },
}); });

4
components/onboarding/ui/Tag.tsx

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
interface TagProps { interface TagProps {
label: string; label: string;
@ -28,7 +28,7 @@ export function TagGroup({ children }: { children: ReactNode }) {
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
group: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 }, group: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm },
tag: { paddingHorizontal: 16, paddingVertical: 9, borderRadius: 18, borderWidth: 1 }, tag: { paddingHorizontal: 16, paddingVertical: 9, borderRadius: 18, borderWidth: 1 },
tagNormal: { backgroundColor: colors.white, borderColor: colors.border }, tagNormal: { backgroundColor: colors.white, borderColor: colors.border },
tagSelected: { backgroundColor: colors.primary, borderColor: colors.primary }, tagSelected: { backgroundColor: colors.primary, borderColor: colors.primary },

6
components/onboarding/ui/WheelPicker.tsx

@ -1,7 +1,7 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
import { ScrollView, StyleSheet, Text, View } from 'react-native'; import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { colors } from '../../../src/theme'; import { colors, spacing } from '../../../src/theme';
import { CURRENT_YEAR } from '../../../src/constants'; import { CURRENT_YEAR } from '../../../src/constants';
const ITEM_H = 44; const ITEM_H = 44;
@ -140,6 +140,6 @@ const styles = StyleSheet.create({
item: { height: ITEM_H, alignItems: 'center', justifyContent: 'center' }, item: { height: ITEM_H, alignItems: 'center', justifyContent: 'center' },
itemText: { fontSize: 18, color: colors.textHint }, itemText: { fontSize: 18, color: colors.textHint },
itemTextActive: { fontSize: 20, fontWeight: '700', color: colors.textPrimary }, itemTextActive: { fontSize: 20, fontWeight: '700', color: colors.textPrimary },
dual: { flexDirection: 'row', justifyContent: 'center', gap: 14 }, dual: { flexDirection: 'row', justifyContent: 'center', gap: spacing.block },
hint: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginTop: 10 }, hint: { fontSize: 13, color: colors.textHint, textAlign: 'center', marginTop: spacing.sm },
}); });

57
scripts/check-spacing.mjs

@ -54,6 +54,19 @@ const STRICT = process.argv.includes('--error');
const ROOTS = ['app', 'components', 'src']; const ROOTS = ['app', 'components', 'src'];
const TARGET = 8; const TARGET = 8;
/**
* 间距令牌镜像(必须与 src/theme/spacing.ts 同步!)。
* 改了 spacing.ts 的取值后,这里不更新会导致审计门禁失准(令牌被当成「无数值」而跳过)。
* 键覆盖标尺(xs/sm/md/lg/xl)与语义令牌(card/gutter/framework/block/section)。
*/
const SPACING_TOKENS = {
xs: 4, sm: 8, md: 12, lg: 16, xl: 24,
card: 8, gutter: 8, framework: 16, block: 12, section: 12,
};
/** 记录本批命中的令牌引用,供输出复核(证明令牌化后审计仍在工作) */
const tokenizedHits = [];
/** 要核对的间距维度 */ /** 要核对的间距维度 */
const CARD_GAP_PROPS = ['marginTop', 'marginBottom']; const CARD_GAP_PROPS = ['marginTop', 'marginBottom'];
@ -125,12 +138,34 @@ function readNumber(text, prop) {
return m ? Number(m[1]) : null; return m ? Number(m[1]) : null;
} }
/**
* 读间距值:先试裸数字,再试令牌引用(spacing.x / spacingTokens.x)。
* 这样卡片 / 留白改成 `spacing.card` 这类写法后,审计仍能解析出数值并核对,
* 不会因为「读不到数字」而把该项静默跳过(那会撕开审计网)。
* 未知令牌名按 null 处理(交给调用方,至多误判,绝不静默通过)。
*/
function resolveSpacing(text, prop) {
const num = readNumber(text, prop);
if (num !== null) return num;
const tm = flatten(text).match(
new RegExp(`^[ \\t]*${prop}:[ \\t]*(?:spacing|spacingTokens)\\.([A-Za-z_$][\\w$]*)`, 'm'),
);
if (tm) {
const v = SPACING_TOKENS[tm[1]];
if (typeof v === 'number') {
tokenizedHits.push(`${prop}: ${tm[1]}`);
return v;
}
}
return null;
}
const hasProp = (text, prop) => new RegExp(`^[ \\t]*${prop}:`, 'm').test(flatten(text)); const hasProp = (text, prop) => new RegExp(`^[ \\t]*${prop}:`, 'm').test(flatten(text));
const isCircle = ({ text }) => { const isCircle = ({ text }) => {
const w = readNumber(text, 'width'); const w = resolveSpacing(text, 'width');
const h = readNumber(text, 'height'); const h = resolveSpacing(text, 'height');
const r = readNumber(text, 'borderRadius'); const r = resolveSpacing(text, 'borderRadius');
return w !== null && h !== null && w === h && Math.abs(r - w / 2) < 0.51; return w !== null && h !== null && w === h && Math.abs(r - w / 2) < 0.51;
}; };
@ -147,7 +182,7 @@ for (const file of files) {
if (!src.includes('StyleSheet.create(')) continue; if (!src.includes('StyleSheet.create(')) continue;
for (const block of styleBlocks(src)) { for (const block of styleBlocks(src)) {
if (readNumber(block.text, 'borderRadius') === null) continue; if (resolveSpacing(block.text, 'borderRadius') === null) continue;
if (!hasProp(block.text, 'backgroundColor') && !hasProp(block.text, 'borderWidth')) continue; if (!hasProp(block.text, 'backgroundColor') && !hasProp(block.text, 'borderWidth')) continue;
if (isCircle(block)) { if (isCircle(block)) {
excluded.push(`${file} | ${block.key} | 圆 ${readNumber(block.text, 'width')}`); excluded.push(`${file} | ${block.key} | 圆 ${readNumber(block.text, 'width')}`);
@ -159,19 +194,19 @@ for (const file of files) {
} }
cardCount += 1; cardCount += 1;
const radius = readNumber(block.text, 'borderRadius'); const radius = resolveSpacing(block.text, 'borderRadius');
if (radius !== TARGET) { if (radius !== TARGET) {
violations.radius.push(`${file} | ${block.key} | borderRadius ${radius}`); violations.radius.push(`${file} | ${block.key} | borderRadius ${radius}`);
continue; continue;
} }
radiusOk += 1; radiusOk += 1;
const padding = readNumber(block.text, 'padding'); const padding = resolveSpacing(block.text, 'padding');
if (padding !== null) { if (padding !== null) {
if (padding !== TARGET) violations.padding.push(`${file} | ${block.key} | padding ${padding}`); if (padding !== TARGET) violations.padding.push(`${file} | ${block.key} | padding ${padding}`);
} else { } else {
for (const prop of ['paddingHorizontal', 'paddingVertical']) { for (const prop of ['paddingHorizontal', 'paddingVertical']) {
const value = readNumber(block.text, prop); const value = resolveSpacing(block.text, prop);
if (value !== null && value !== TARGET) { if (value !== null && value !== TARGET) {
violations.padding.push(`${file} | ${block.key} | ${prop} ${value}`); violations.padding.push(`${file} | ${block.key} | ${prop} ${value}`);
} }
@ -179,7 +214,7 @@ for (const file of files) {
} }
for (const prop of CARD_GAP_PROPS) { for (const prop of CARD_GAP_PROPS) {
const value = readNumber(block.text, prop); const value = resolveSpacing(block.text, prop);
if (value !== null && value !== TARGET) { if (value !== null && value !== TARGET) {
violations.gap.push(`${file} | ${block.key} | ${prop} ${value}`); violations.gap.push(`${file} | ${block.key} | ${prop} ${value}`);
} }
@ -206,8 +241,8 @@ for (const file of files) {
for (const key of candidates) { for (const key of candidates) {
const block = blocks.find((b) => b.key === key); const block = blocks.find((b) => b.key === key);
if (!block) continue; if (!block) continue;
const padding = readNumber(block.text, 'padding'); const padding = resolveSpacing(block.text, 'padding');
const horizontal = readNumber(block.text, 'paddingHorizontal'); const horizontal = resolveSpacing(block.text, 'paddingHorizontal');
if (padding === null && horizontal === null) continue; if (padding === null && horizontal === null) continue;
const value = padding !== null ? padding : horizontal; const value = padding !== null ? padding : horizontal;
if (value !== TARGET) violations.gutter.push(`${file} | ${key} | ${value}`); if (value !== TARGET) violations.gutter.push(`${file} | ${key} | ${value}`);
@ -229,6 +264,8 @@ console.log(` 卡片间隔 ≠ ${TARGET}:${violations.gap.length}`);
violations.gap.forEach((line) => console.log(` ${line}`)); violations.gap.forEach((line) => console.log(` ${line}`));
console.log(` 页面留白 ≠ ${TARGET}:${violations.gutter.length}`); console.log(` 页面留白 ≠ ${TARGET}:${violations.gutter.length}`);
violations.gutter.forEach((line) => console.log(` ${line}`)); violations.gutter.forEach((line) => console.log(` ${line}`));
console.log(`--- 令牌引用命中 ${tokenizedHits.length} 处(spacing.* / spacingTokens.*,审计已解析) ---`);
tokenizedHits.forEach((line) => console.log(` ${line}`));
console.log(`--- 已剔除 ${excluded.length} 项(圆 / 控件),逐条列出供复核 ---`); console.log(`--- 已剔除 ${excluded.length} 项(圆 / 控件),逐条列出供复核 ---`);
excluded.forEach((line) => console.log(` ${line}`)); excluded.forEach((line) => console.log(` ${line}`));

5
src/theme/index.ts

@ -1,6 +1,11 @@
/** /**
* 主题统一导出 * 主题统一导出
*/ */
export {
spacing,
spacingTokens,
} from './spacing';
export { export {
colors, colors,
darkColors, darkColors,

42
src/theme/spacing.ts

@ -0,0 +1,42 @@
/**
* 间距设计令牌(Spacing Design Tokens):全局间距单一来源。
*
* 对应 AGENTS.md「所有卡片圆角 8px、边距间隔一致 8px」规则,以及页面框架 / 非卡片间距的统一标尺。
* 此前 8px 与各间距值散落为裸字面量(见《项目开发状态.md》4.18 / 4.19 / 4.20):
* - check-spacing.mjs 能拦「值跑偏」,但「改一次值要动 N 个文件」;
* - 非卡片间距(图标底块、段间距)在多个页面取 12 / 18 / 22,同角色不同值,无单一来源。
* 本文件把间距收口为单一来源,新增样式一律从这里取。
* 与 colors.ts 保持同一风格:先定义标尺,再叠加语义令牌,全部挂在 `spacing` 一个对象上,
* 页面只需 `import { spacing }`,写 `spacing.block` / `spacing.section` 即可。
*
* 重要:scripts/check-spacing.mjs 顶部有一份 SPACING_TOKENS 镜像映射,
* 改动本文件的取值后必须同步更新脚本里的镜像,否则审计门禁会失准。
*/
/** 4 基准间距标尺(与现有使用习惯对齐:卡片 8 / 块 12 / 框架 16 / 大间距 24) */
export const spacing = {
xs: 4,
sm: 8, // 卡片圆角 / 卡片内边距 / 卡片间隔 / 页面留白(AGENTS.md 基准)
md: 12, // 非卡片块间距标尺
lg: 16, // 页面框架标尺(头部返回区 / 底部固定区)
xl: 24, // 大段间距
/** 语义令牌:与 check-spacing.mjs 四维度及页面框架一一对应,改值只动这里 */
card: 8, // 卡片圆角 / 内边距 / 间隔 / 页面留白
gutter: 8, // 主内容 ScrollView 留白
framework: 16, // 页面框架(header 返回区 / footer 固定区,有意比卡片宽)
block: 12, // 非卡片块间距(图标底块、字段→提示)
section: 12, // 段标题上下间距
} as const;
/**
* 语义令牌别名(与 `spacing` 同名键取值一致),保留以兼容审计脚本对 `spacingTokens.x` 的解析。
* 页面请直接用 `spacing.block` / `spacing.section`,无需引用本对象。
*/
export const spacingTokens = {
card: spacing.card,
gutter: spacing.gutter,
framework: spacing.framework,
block: spacing.block,
section: spacing.section,
} as const;

36
项目开发状态.md

@ -1771,6 +1771,42 @@ deep-link 只能到 P1。**临时**把初始步改成目标页截图,**看完
- 引导页三屏按钮文案是否统一为「开启体验」,见第 3 条末 —— 一行改动,等一句话。 - 引导页三屏按钮文案是否统一为「开启体验」,见第 3 条末 —— 一行改动,等一句话。
- 4.19 第 9 条的三项(主应用 footer 16 vs 卡片 8、非卡片间距、设计令牌层)**均未动**。 - 4.19 第 9 条的三项(主应用 footer 16 vs 卡片 8、非卡片间距、设计令牌层)**均未动**。
### 4.21 间距设计令牌层 + onboarding 非卡片间距收敛(2026-09-21 批次,接 4.20 第 6 条末)
**起因**:4.18/4.19/4.20 把卡片圆角 / 内边距 / 间隔 / 留白统一到了 8px,但**间距仍全是裸字面量**,两个遗留问题:
① 改一次间距值要动 N 个文件;② 非卡片间距(图标底块、段标题上下、`tip` 提示距离)在多个引导页取 12/14/18/22,**同角色不同值,无单一来源**;
③ 更重要的是——`check-spacing.mjs` 是 `exit 1` 硬门槛,一旦把卡片改成 `spacing.card` 这类引用,原脚本读不到数字会**静默跳过该卡片**,审计网被撕开。
**1. 新增间距单一来源 `src/theme/spacing.ts`**(形式对齐 `colors.ts`)
- 标尺:`xs:4 / sm:8 / md:12 / lg:16 / xl:24`。
- 语义令牌(挂在同一个 `spacing` 对象上,页面只 `import { spacing }` 即可):`card:8 / gutter:8 / framework:16 / block:12 / section:12`。
- 从 `src/theme/index.ts` 导出。
**2. 扩展 `scripts/check-spacing.mjs`,让令牌化不失效**(核心防回归)
- 顶部加 `SPACING_TOKENS` 镜像映射(必须与 `spacing.ts` 取值同步,文件头已写警告)。
- 新增 `resolveSpacing(text, prop)`:`spacing.x` / `spacingTokens.x` 都能解析成数值;未知令牌名按 null(至多误判,绝不静默通过)。
- 卡片判定(原 `readNumber(...) === null → continue`)与 `isCircle` 改用 `resolveSpacing`——**修掉一个潜在 bug**:原逻辑对令牌化的 `borderRadius` 会读成 null 直接跳过卡片。
- 输出新增「令牌引用命中 N 处」一行,证明令牌化后审计仍在工作。
- **自检证明门禁存活**:注入临时文件,令牌化的 `borderRadius: spacing.lg(16)` / `padding: spacing.framework(16)` / 留白 `padding: spacing.md(12)` 三处均被抓出并 `exit 1`;合法令牌化卡片(`spacing.card`=8)通过。临时文件已删除。
**3. onboarding 非卡片间距令牌化 + 收敛(35 个文件)**
- 仅改写间距属性(`margin*` / `gap` / `paddingVertical` 等,**绝不碰 `fontSize`**),按值映射:
`18 → block(12)`、`22 → section(12)`、`14 → block(12)`、`10 → sm(8)`、`原 12 → block`。
- 每个文件补 `spacing` 导入(导入路径已确认统一为 `../../../src/theme`)。
- 收敛效果:段标题上下、提示距离、图标底块间距统一到 12,原先 14/18/22 的漂移被收口。
**4. 本批明确延期:B5(app/ 框架 `padding:16` → `spacing.framework`)**
- 框架 16px 是**有意比卡片宽**(页面骨架),且不被审计;改 26 个 app 文件导入路径多样、回归风险高、收益有限。
- `spacing.framework` 单一来源已存在,后续可单独一批做;延期不阻断任何门禁。
**5. 验证(全绿)**
`tsc` 0 · `eslint` 0 · `npm test` 26 套件 / 365 用例 · `check:spacing` 卡片全 8px · `check:routes` · `check:affordances` · `check:icons` 全绿。
**6. 残留 / 待跟进**
- ① **卡片未批量迁移到 `spacing.card`**:保留字面量 `8`,审计仍能拦(迁移是纯机械替换,风险低但非必须,留待后续)。
- ② B5 延期(见第 4 条)。
- ③ `AGENTS.md` 第 10 行仍写「边距间隔一致,为 8px」,现非卡片块间距为 12,措辞略偏——非阻断,待文档对齐(可改成「卡片间距 8px / 块间距 12px」)。
--- ---
## 五、后续增量开发建议(优先级) ## 五、后续增量开发建议(优先级)

Loading…
Cancel
Save