Files
richarjiang 39671ed70f feat(challenges): 添加自定义挑战功能和多语言支持
- 新增自定义挑战创建页面,支持设置挑战类型、时间范围、目标值等
- 实现挑战邀请码系统,支持通过邀请码加入自定义挑战
- 完善挑战详情页面的多语言翻译支持
- 优化用户认证状态检查逻辑,使用token作为主要判断依据
- 添加阿里字体文件支持,提升UI显示效果
- 改进确认弹窗组件,支持Liquid Glass效果和自定义内容
- 优化应用启动流程,直接读取onboarding状态而非预加载用户数据
2025-11-26 16:39:01 +08:00

20 KiB
Raw Permalink Blame History

常见任务和模式

图标库使用规范 - 禁止使用 MaterialIcons

最后更新: 2025-10-24

重要规则

项目中不允许使用 MaterialIcons,所有图标必须使用 Ionicons 以保持图标库的一致性。

问题描述

在项目中发现使用 MaterialIcons 的情况,需要将所有 MaterialIcons 替换为 Ionicons以保持图标库的一致性。

解决方案

将所有 MaterialIcons 导入和使用替换为对应的 Ionicons。

实现模式

1. 替换导入语句

// ❌ 禁止使用
import { MaterialIcons } from "@expo/vector-icons";

// ✅ 正确写法
import { Ionicons } from "@expo/vector-icons";

2. 替换图标名称和属性

// ❌ 禁止使用
<MaterialIcons name="arrow-back-ios" size={20} color="#333" />

// ✅ 正确写法 - 使用 HeaderBar 中的返回按钮实现
<Ionicons name="chevron-back" size={24} color="#333" />

3. 常见图标映射

  • arrow-back-ioschevron-back (返回按钮)
  • auto-awesomestar (星星/自动推荐)
  • tips-and-updatesbulb (提示/建议)
  • fact-checkcheckbox (检查/确认)
  • check-circlecheckmark-circle (勾选圆圈)
  • removeremove (移除/删除,名称相同)

重要注意事项

  1. 图标大小调整Ionicons 和 MaterialIcons 的默认大小可能不同,需要适当调整
  2. 图标名称差异:两个图标库的图标名称不同,需要找到对应的功能图标
  3. 样式一致性:确保替换后的图标在视觉上与原设计保持一致
  4. Liquid Glass 兼容性:替换后的图标需要继续支持 Liquid Glass 效果
  5. 代码审查:在代码审查中需要特别检查是否使用了 MaterialIcons

参考实现

  • components/ui/HeaderBar.tsx - 返回按钮的标准实现
  • components/model/MembershipModal.tsx - 完整的 MaterialIcons 替换示例

按钮组件 Liquid Glass 兼容性

最后更新: 2025-10-24

重要原则

所有按钮组件都需要尝试兼容 Liquid Glass,这是项目的设计要求。

实现模式

1. 导入必要的组件

import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";

2. 检查设备支持情况

const isGlassAvailable = isLiquidGlassAvailable();

3. 实现条件渲染的按钮

<TouchableOpacity
  onPress={handlePress}
  disabled={isLoading}
  activeOpacity={0.7}
>
  {isLiquidGlassAvailable() ? (
    <GlassView
      style={styles.button}
      glassEffectStyle="clear" // 或 "regular"
      tintColor="rgba(255, 255, 255, 0.3)" // 自定义色调
      isInteractive={true} // 启用交互反馈
    >
      <Ionicons name="icon-name" size={20} color="#333" />
    </GlassView>
  ) : (
    <View style={[styles.button, styles.fallbackButton]}>
      <Ionicons name="icon-name" size={20} color="#333" />
    </View>
  )}
</TouchableOpacity>

4. 定义样式

const styles = StyleSheet.create({
  button: {
    width: 40,
    height: 40,
    borderRadius: 20, // 圆形按钮
    alignItems: "center",
    justifyContent: "center",
    overflow: "hidden", // 保证玻璃边界圆角效果
    // 其他通用样式...
  },
  fallbackButton: {
    backgroundColor: "rgba(255, 255, 255, 0.9)",
    borderWidth: 1,
    borderColor: "rgba(255, 255, 255, 0.3)",
  },
});

重要注意事项

  1. 兼容性检查:必须使用 isLiquidGlassAvailable() 检查设备支持情况
  2. overflow: 'hidden'GlassView 组件需要设置此属性以保证圆角效果
  3. 降级样式:为不支持 Liquid Glass 的设备提供视觉上相似的替代方案
  4. 交互反馈:设置 isInteractive={true} 启用原生的触觉反馈
  5. 图标居中:确保使用 alignItems: 'center'justifyContent: 'center' 使图标完全居中
  6. 色调自定义:通过 tintColor 属性自定义按钮的颜色主题

常用配置

  • glassEffectStyle: "clear"(透明)或 "regular"(常规)
  • tintColor: 根据按钮功能选择合适的颜色
    • 返回/导航操作:白色系 rgba(255, 255, 255, 0.3)
    • 删除操作:红色系 rgba(244, 67, 54, 0.2)
    • 确认操作:绿色系 rgba(76, 175, 80, 0.2)
    • 信息操作:蓝色系 rgba(33, 150, 243, 0.2)

参考实现

  • components/model/MembershipModal.tsx - 悬浮返回按钮
  • components/glass/button.tsx - 通用 Glass 按钮组件
  • app/(tabs)/_layout.tsx - 标签栏按钮实现

HeaderBar 顶部距离处理

最后更新: 2025-10-16

问题描述

当使用 HeaderBar 组件时,需要正确处理内容区域的顶部距离,确保内容不会被状态栏或刘海屏遮挡。

解决方案

使用 useSafeAreaTop hook 获取安全区域顶部距离,并应用到内容容器的样式中。

实现模式

1. 导入必要的 hook

import { useSafeAreaTop } from "@/hooks/useSafeAreaWithPadding";

2. 在组件中获取 safeAreaTop

const safeAreaTop = useSafeAreaTop();

3. 应用到内容容器

// 方式1: 直接应用到 View 组件
<View style={[styles.filterContainer, { paddingTop: safeAreaTop }]}>

// 方式2: 应用到 ScrollView 的 contentContainerStyle
<ScrollView
  contentContainerStyle={{ paddingTop: safeAreaTop }}
>

// 方式3: 应用到 SectionList 的 style
<SectionList
  style={{ paddingTop: safeAreaTop }}
>

重要注意事项

  1. 不要在 StyleSheet 中使用变量:不能在 StyleSheet.create() 中直接使用 safeAreaTop 变量
  2. 使用动态样式:必须通过内联样式或数组样式的方式动态应用 safeAreaTop
  3. 不需要额外偏移:通常只需要 safeAreaTop,不需要添加额外的固定像素值

示例代码

// ❌ 错误写法 - 在 StyleSheet 中使用变量
const styles = StyleSheet.create({
  filterContainer: {
    paddingTop: safeAreaTop, // 这会导致错误
  },
});

// ✅ 正确写法 - 使用动态样式
<View style={[styles.filterContainer, { paddingTop: safeAreaTop }]}>

参考页面

  • app/steps/detail.tsx
  • app/water/detail.tsx
  • app/profile/goals.tsx
  • app/workout/history.tsx
  • app/challenges/[id]/leaderboard.tsx

Liquid Glass 风格图标按钮实现

最后更新: 2025-10-16

问题描述

在应用中实现符合 Liquid Glass 设计风格的图标按钮,需要考虑毛玻璃效果和兼容性处理。

解决方案

使用 GlassView 组件实现毛玻璃效果,并提供不支持 Liquid Glass 的设备的降级方案。

实现模式

1. 导入必要的组件和函数

import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";

2. 检查设备支持情况

const isGlassAvailable = isLiquidGlassAvailable();

3. 实现条件渲染的按钮

<TouchableOpacity
  onPress={handlePress}
  disabled={isLoading}
  activeOpacity={0.7}
>
  {isGlassAvailable ? (
    <GlassView
      style={styles.glassButton}
      glassEffectStyle="clear" // 或 "regular"
      tintColor="rgba(244, 67, 54, 0.2)" // 自定义色调
      isInteractive={true} // 启用交互反馈
    >
      <Ionicons name="trash-outline" size={20} color="#F44336" />
    </GlassView>
  ) : (
    <View style={[styles.glassButton, styles.fallbackButton]}>
      <Ionicons name="trash-outline" size={20} color="#F44336" />
    </View>
  )}
</TouchableOpacity>

4. 定义样式

const styles = StyleSheet.create({
  glassButton: {
    width: 36,
    height: 36,
    borderRadius: 18, // 圆形按钮
    alignItems: "center",
    justifyContent: "center",
    overflow: "hidden", // 保证玻璃边界圆角效果
  },
  fallbackButton: {
    borderWidth: 1,
    borderColor: "rgba(244, 67, 54, 0.3)",
    backgroundColor: "rgba(244, 67, 54, 0.1)",
  },
});

重要注意事项

  1. 兼容性处理:必须使用 isLiquidGlassAvailable() 检查设备支持情况
  2. overflow: 'hidden'GlassView 组件需要设置此属性以保证圆角效果
  3. 降级样式:为不支持 Liquid Glass 的设备提供视觉上相似的替代方案
  4. 交互反馈:设置 isInteractive={true} 启用原生的触觉反馈
  5. 色调自定义:通过 tintColor 属性自定义按钮的颜色主题

常用配置

  • glassEffectStyle: "clear"(透明)或 "regular"(常规)
  • tintColor: 根据按钮功能选择合适的颜色
    • 删除操作:红色系 rgba(244, 67, 54, 0.2)
    • 确认操作:绿色系 rgba(76, 175, 80, 0.2)
    • 信息操作:蓝色系 rgba(33, 150, 243, 0.2)

参考实现

  • app/food/nutrition-analysis-history.tsx - 删除按钮实现
  • components/glass/button.tsx - 通用 Glass 按钮组件
  • app/(tabs)/_layout.tsx - 标签栏按钮实现

登录验证实现模式

最后更新: 2025-10-16

问题描述

在应用中实现需要登录才能访问的功能时,需要判断用户是否已登录,未登录时先跳转到登录页面。

解决方案

使用 useAuthGuard hook 中的 pushIfAuthedElseLogin 方法处理需要登录验证的导航操作,使用 ensureLoggedIn 方法处理需要登录验证的功能实现。

权限校验原则

重要: 功能实现如果包含服务端接口的调用,需要使用 ensureLoggedIn 来判断用户是否登录。

实现模式

1. 导入必要的 hook

import { useAuthGuard } from "@/hooks/useAuthGuard";

2. 在组件中获取方法

const { pushIfAuthedElseLogin, ensureLoggedIn } = useAuthGuard();

3. 替换导航操作

// ❌ 原来的写法 - 没有登录验证
<TouchableOpacity
  onPress={() => router.push('/food/nutrition-analysis-history')}
  activeOpacity={0.7}
>

// ✅ 修改后的写法 - 带登录验证
<TouchableOpacity
  onPress={() => pushIfAuthedElseLogin('/food/nutrition-analysis-history')}
  activeOpacity={0.7}
>

4. 服务端接口调用的登录验证

对于需要调用服务端接口的功能,使用 ensureLoggedIn 进行登录验证:

// ❌ 原来的写法 - 没有登录验证
<TouchableOpacity
  onPress={() => startNewAnalysis(imageUri)}
  activeOpacity={0.8}
>

// ✅ 修改后的写法 - 带登录验证
<TouchableOpacity
  onPress={async () => {
    // 先验证登录状态
    const isLoggedIn = await ensureLoggedIn();
    if (isLoggedIn) {
      startNewAnalysis(imageUri);
    }
  }}
  activeOpacity={0.8}
>

5. 完整示例(包含 Liquid Glass 兼容性处理)

{
  isLiquidGlassAvailable() ? (
    <TouchableOpacity
      onPress={() => pushIfAuthedElseLogin("/food/nutrition-analysis-history")}
      activeOpacity={0.7}
    >
      <GlassView
        style={styles.historyButton}
        glassEffectStyle="clear"
        tintColor="rgba(255, 255, 255, 0.2)"
        isInteractive={true}
      >
        <Ionicons name="time-outline" size={24} color="#333" />
      </GlassView>
    </TouchableOpacity>
  ) : (
    <TouchableOpacity
      onPress={() => pushIfAuthedElseLogin("/food/nutrition-analysis-history")}
      style={[styles.historyButton, styles.fallbackBackground]}
      activeOpacity={0.7}
    >
      <Ionicons name="time-outline" size={24} color="#333" />
    </TouchableOpacity>
  );
}

重要注意事项

  1. 统一体验:使用 pushIfAuthedElseLogin 可以确保登录后自动跳转到目标页面
  2. 参数传递:该方法支持传递路由参数,格式为 pushIfAuthedElseLogin('/path', { param: value })
  3. 登录重定向:登录页面会接收 redirectToredirectParams 参数用于登录后跳转
  4. 兼容性:与 Liquid Glass 设计风格完全兼容,可以同时使用
  5. 服务端接口调用:所有调用服务端接口的功能必须使用 ensureLoggedIn 进行登录验证
  6. 异步处理ensureLoggedIn 是异步函数,需要使用 await 等待结果

其他可用方法

  • ensureLoggedIn() - 检查登录状态,未登录时跳转到登录页面,返回布尔值表示是否已登录
  • guardHandler(fn, options) - 包装一个函数,在执行前确保用户已登录
  • isLoggedIn - 布尔值,表示当前用户是否已登录

使用场景选择

  • 页面导航:使用 pushIfAuthedElseLogin 处理页面跳转
  • 服务端接口调用:使用 ensureLoggedIn 验证登录状态后再执行功能
  • 函数包装:使用 guardHandler 包装需要登录验证的函数

参考实现

  • app/food/nutrition-label-analysis.tsx - 成分表分析功能登录验证
  • app/(tabs)/personal.tsx - 个人中心编辑按钮
  • hooks/useAuthGuard.ts - 完整的认证守卫实现

路由常量管理

最后更新: 2025-10-16

问题描述

在应用开发中,所有路由路径都应该使用常量定义,而不是硬编码字符串。这样可以确保路由的一致性,便于维护和重构。

解决方案

将所有路由路径定义在 constants/Routes.ts 文件中,并在组件中使用这些常量。

实现模式

1. 添加新路由常量

constants/Routes.ts 文件中添加新的路由常量:

export const ROUTES = {
  // 现有路由...

  // 新增路由
  FOOD_CAMERA: "/food/camera",
} as const;

2. 在组件中使用路由常量

导入并使用路由常量,而不是硬编码路径:

import { ROUTES } from "@/constants/Routes";

// ❌ 错误写法 - 硬编码路径
router.push("/food/camera?mealType=dinner");

// ✅ 正确写法 - 使用路由常量
router.push(`${ROUTES.FOOD_CAMERA}?mealType=dinner`);

3. 结合登录验证使用

对于需要登录验证的路由,结合 pushIfAuthedElseLogin 使用:

import { ROUTES } from '@/constants/Routes';
import { useAuthGuard } from '@/hooks/useAuthGuard';

const { pushIfAuthedElseLogin } = useAuthGuard();

// 在需要登录验证的路由中使用
<TouchableOpacity
  onPress={() => pushIfAuthedElseLogin(`${ROUTES.FOOD_CAMERA}?mealType=${currentMealType}`)}
  activeOpacity={0.7}
>

重要注意事项

  1. 统一管理:所有路由路径都必须在 constants/Routes.ts 中定义
  2. 命名规范:使用大写字母和下划线,如 FOOD_CAMERA
  3. 路径一致性:常量名应该清晰表达路由的用途
  4. 参数处理:查询参数和路径参数在使用时动态拼接
  5. 类型安全:使用 as const 确保类型推导

路由分类

按照功能模块对路由进行分组:

export const ROUTES = {
  // Tab路由
  TAB_EXPLORE: "/explore",
  TAB_COACH: "/coach",

  // 营养相关路由
  NUTRITION_RECORDS: "/nutrition/records",
  FOOD_LIBRARY: "/food-library",
  FOOD_CAMERA: "/food/camera",

  // 用户相关路由
  AUTH_LOGIN: "/auth/login",
  PROFILE_EDIT: "/profile/edit",
} as const;

参考实现

  • constants/Routes.ts - 路由常量定义
  • components/NutritionRadarCard.tsx - 使用路由常量和登录验证
  • app/food/camera.tsx - 食物拍照页面实现

多语言翻译实现规范

最后更新: 2025-11-26

重要原则

所有用户可见的文本都必须支持多语言翻译,这是项目的基本要求。不允许在代码中硬编码任何用户可见的中文或英文文本。

问题描述

在开发新功能或修改现有功能时,所有用户界面文本都需要支持多语言切换,确保应用能够为不同语言用户提供本地化体验。

解决方案

使用项目集成的 i18next 翻译系统,在 i18n/index.ts 中定义翻译资源,在组件中使用 useI18n hook 获取翻译文本。

实现模式

1. 导入必要的 hook

import { useI18n } from "@/hooks/useI18n";

2. 在组件中获取翻译函数

const { t } = useI18n();

3. 添加翻译资源

i18n/index.ts 中为新的功能模块添加翻译资源:

// 中文翻译
const newFeatureResources = {
  title: "新功能标题",
  subtitle: "新功能描述",
  button: "按钮文本",
  loading: "加载中...",
  error: "操作失败,请稍后重试",
  success: "操作成功",
};

// 英文翻译
const newFeatureResourcesEn = {
  title: "New Feature Title",
  subtitle: "New feature description",
  button: "Button Text",
  loading: "Loading...",
  error: "Operation failed, please try again later",
  success: "Operation successful",
};

// 添加到资源对象中
resources = {
  zh: {
    translation: {
      // 现有翻译...
      newFeature: newFeatureResources,
    },
  },
  en: {
    translation: {
      // 现有翻译...
      newFeature: newFeatureResourcesEn,
    },
  },
};

4. 在组件中使用翻译

// ❌ 错误写法 - 硬编码文本
<Text>加载中...</Text>
<Text>操作失败,请稍后重试</Text>

// ✅ 正确写法 - 使用翻译函数
<Text>{t('newFeature.loading')}</Text>
<Text>{t('newFeature.error')}</Text>

5. 动态参数翻译

对于包含动态参数的文本,使用插值语法:

// 翻译资源中
welcome: '欢迎,{{name}}'
itemsCount: '共 {{count}} 个项目'

// 组件中使用
<Text>{t('newFeature.welcome', { name: userName })}</Text>
<Text>{t('newFeature.itemsCount', { count: items.length })}</Text>

6. 嵌套翻译键

对于复杂功能,使用嵌套的翻译键结构:

// 翻译资源
modal: {
  title: '确认操作',
  description: '确定要执行此操作吗?',
  buttons: {
    confirm: '确认',
    cancel: '取消',
  },
}

// 组件中使用
<Text>{t('newFeature.modal.title')}</Text>
<Text>{t('newFeature.modal.buttons.confirm')}</Text>

重要注意事项

  1. 禁止硬编码:所有用户可见的文本都必须通过翻译函数获取
  2. 完整翻译:中文和英文翻译都必须提供,保持翻译完整性
  3. 语义化命名:翻译键应该清晰表达文本的用途和含义
  4. 参数化文本:包含动态内容的文本应该使用插值参数
  5. 一致性:相同功能的文本应该使用相同的翻译键
  6. Toast 消息Toast 提示消息也需要翻译支持
  7. 错误消息:错误提示信息必须支持多语言
  8. 表单验证:表单验证错误信息需要翻译

常见翻译模式

1. 状态文本

status: {
  loading: '加载中...',
  success: '操作成功',
  error: '操作失败',
  empty: '暂无数据',
}

2. 按钮文本

buttons: {
  confirm: '确认',
  cancel: '取消',
  save: '保存',
  delete: '删除',
  edit: '编辑',
  add: '添加',
}

3. 表单相关

form: {
  placeholders: {
    email: '请输入邮箱地址',
    password: '请输入密码',
  },
  errors: {
    required: '此字段为必填项',
    invalid: '格式不正确',
  },
}

4. 列表和表格

list: {
  empty: '暂无数据',
  loading: '加载中...',
  loadMore: '加载更多',
  refresh: '刷新',
}

翻译键命名规范

  1. 使用小写字母和点号分隔feature.section.item
  2. 按功能模块分组challenges.title, challenges.subtitle
  3. 语义化命名buttons.confirm, errors.network
  4. 避免缩写:使用 description 而不是 desc

参考实现

  • app/(tabs)/challenges.tsx - 完整的多语言翻译实现示例
  • i18n/index.ts - 翻译资源配置
  • hooks/useI18n.ts - 翻译 hook 实现
  • app/(tabs)/personal.tsx - 个人中心页面翻译实现
  • app/food/nutrition-label-analysis.tsx - 营养分析页面翻译实现

检查清单

在开发新功能时,请确保:

  • 所有用户可见的文本都使用了翻译函数
  • i18n/index.ts 中添加了对应的中文和英文翻译
  • Toast 消息支持多语言
  • 错误提示信息支持多语言
  • 表单验证错误信息支持多语言
  • 动态参数文本使用了插值语法
  • 翻译键命名符合规范
  • 测试了语言切换功能

最佳实践

  1. 开发时即考虑多语言:在编写组件时就使用翻译函数,而不是事后添加
  2. 保持翻译一致性:相同含义的文本使用相同的翻译键
  3. 定期审查:定期检查是否有硬编码文本遗漏
  4. 测试验证:在开发完成后测试语言切换功能是否正常