Files
digital-pilates/components/challenges/ChallengeRankingItem.tsx
richarjiang fbe0c92f0f feat(i18n): 全面实现应用核心功能模块的国际化支持
- 新增 i18n 翻译资源,覆盖睡眠、饮水、体重、锻炼、用药 AI 识别、步数、健身圆环、基础代谢及设置等核心模块
- 重构相关页面及组件(如 SleepDetail, WaterDetail, WorkoutHistory 等)使用 `useI18n` 钩子替换硬编码文本
- 升级 `utils/date` 工具库与 `DateSelector` 组件,支持基于语言环境的日期格式化与显示
- 完善登录页、注销流程及权限申请弹窗的双语提示信息
- 优化部分页面的 UI 细节与字体样式以适配多语言显示
2025-11-27 17:54:36 +08:00

145 lines
3.9 KiB
TypeScript

import { useI18n } from '@/hooks/useI18n';
import type { RankingItem } from '@/store/challengesSlice';
import { Ionicons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
type ChallengeRankingItemProps = {
item: RankingItem;
index: number;
showDivider?: boolean;
unit?: string;
};
const formatNumber = (value: number): string => {
if (Number.isInteger(value)) {
return value.toString();
}
return value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
};
export function ChallengeRankingItem({ item, index, showDivider = false, unit }: ChallengeRankingItemProps) {
const { t } = useI18n();
const formatMinutes = (value: number): string => {
const safeValue = Math.max(0, Math.round(value));
const hours = safeValue / 60;
return `${hours.toFixed(1)} ${t('challengeDetail.ranking.hour')}`;
};
const formatValueWithUnit = (value: number | undefined, unit?: string): string | undefined => {
if (typeof value !== 'number' || Number.isNaN(value)) {
return undefined;
}
if (unit === 'min') {
return formatMinutes(value);
}
const formatted = formatNumber(value);
return unit ? `${formatted} ${unit}` : formatted;
};
const reportedLabel = formatValueWithUnit(item.todayReportedValue, unit);
const targetLabel = formatValueWithUnit(item.todayTargetValue, unit);
const progressLabel = reportedLabel && targetLabel
? `${t('challengeDetail.ranking.today')} ${reportedLabel} / ${targetLabel}`
: reportedLabel
? `${t('challengeDetail.ranking.today')} ${reportedLabel}`
: targetLabel
? `${t('challengeDetail.ranking.todayGoal')} ${targetLabel}`
: undefined;
return (
<View style={[styles.rankingRow, showDivider && styles.rankingRowDivider]}>
<View style={styles.rankingOrderCircle}>
<Text style={styles.rankingOrder}>{index + 1}</Text>
</View>
{item.avatar ? (
<Image source={{ uri: item.avatar }} style={styles.rankingAvatar} cachePolicy="memory-disk" />
) : (
<View style={styles.rankingAvatarPlaceholder}>
<Ionicons name="person-outline" size={20} color="#6f7ba7" />
</View>
)}
<View style={styles.rankingInfo}>
<Text style={styles.rankingName} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.rankingMetric}>{item.metric}</Text>
{progressLabel ? (
<Text style={styles.rankingProgress} numberOfLines={1}>
{progressLabel}
</Text>
) : null}
</View>
{item.badge ? <Text style={styles.rankingBadge}>{item.badge}</Text> : null}
</View>
);
}
const styles = StyleSheet.create({
rankingRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 18,
},
rankingRowDivider: {
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#E5E7FF',
},
rankingOrderCircle: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#EEF0FF',
marginRight: 12,
},
rankingOrder: {
fontSize: 15,
fontWeight: '700',
color: '#4F5BD5',
},
rankingAvatar: {
width: 44,
height: 44,
borderRadius: 22,
marginRight: 14,
backgroundColor: '#EEF0FF',
},
rankingAvatarPlaceholder: {
width: 44,
height: 44,
borderRadius: 22,
marginRight: 14,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#EEF0FF',
},
rankingInfo: {
flex: 1,
},
rankingName: {
fontSize: 15,
fontWeight: '700',
color: '#1c1f3a',
},
rankingMetric: {
marginTop: 4,
fontSize: 12,
color: '#6f7ba7',
},
rankingProgress: {
marginTop: 2,
fontSize: 10,
color: '#8a94c1',
},
rankingBadge: {
fontSize: 12,
color: '#A67CFF',
fontWeight: '700',
},
});