feat(challenges): 添加自定义挑战类型并优化字段验证
- 新增 CUSTOM 挑战类型支持 - 移除 requirementLabel 必填验证,改为可选字段 - 添加挑战类型选择器的编辑模式禁用状态 - 优化日期选择器的多语言支持 - 完善中英文国际化文案 - 修复空 requirementLabel 导致的渲染问题
This commit is contained in:
@@ -595,7 +595,7 @@ export default function ChallengeDetailScreen() {
|
|||||||
<Ionicons name="flag-outline" size={20} color="#5E8BFF" />
|
<Ionicons name="flag-outline" size={20} color="#5E8BFF" />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.shareInfoTextWrapper}>
|
<View style={styles.shareInfoTextWrapper}>
|
||||||
<Text style={styles.shareInfoLabel}>{challenge.requirementLabel}</Text>
|
{challenge.requirementLabel ? <Text style={styles.shareInfoLabel}>{challenge.requirementLabel}</Text> : null}
|
||||||
<Text style={styles.shareInfoMeta}>{t('challengeDetail.shareCard.info.checkInDaily')}</Text>
|
<Text style={styles.shareInfoMeta}>{t('challengeDetail.shareCard.info.checkInDaily')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -743,7 +743,7 @@ export default function ChallengeDetailScreen() {
|
|||||||
<Ionicons name="flag-outline" size={20} color="#4F5BD5" />
|
<Ionicons name="flag-outline" size={20} color="#4F5BD5" />
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.detailTextWrapper}>
|
<View style={styles.detailTextWrapper}>
|
||||||
<Text style={styles.detailLabel}>{challenge.requirementLabel}</Text>
|
{challenge.requirementLabel ? <Text style={styles.detailLabel}>{challenge.requirementLabel}</Text> : null}
|
||||||
<Text style={styles.detailMeta}>{t('challengeDetail.detail.requirement')}</Text>
|
<Text style={styles.detailMeta}>{t('challengeDetail.detail.requirement')}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import i18n from '@/i18n';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
import * as Clipboard from 'expo-clipboard';
|
import * as Clipboard from 'expo-clipboard';
|
||||||
@@ -53,6 +54,7 @@ const getTypeOptions = (t: (key: string) => string): { value: ChallengeType; lab
|
|||||||
{ value: ChallengeType.SLEEP, label: t('challenges.createCustom.typeLabels.sleep'), accent: '#7C3AED' },
|
{ value: ChallengeType.SLEEP, label: t('challenges.createCustom.typeLabels.sleep'), accent: '#7C3AED' },
|
||||||
{ value: ChallengeType.MOOD, label: t('challenges.createCustom.typeLabels.mood'), accent: '#F97316' },
|
{ value: ChallengeType.MOOD, label: t('challenges.createCustom.typeLabels.mood'), accent: '#F97316' },
|
||||||
{ value: ChallengeType.WEIGHT, label: t('challenges.createCustom.typeLabels.weight'), accent: '#22C55E' },
|
{ value: ChallengeType.WEIGHT, label: t('challenges.createCustom.typeLabels.weight'), accent: '#22C55E' },
|
||||||
|
{ value: ChallengeType.CUSTOM, label: t('challenges.createCustom.typeLabels.custom'), accent: '#8B5CF6' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const FALLBACK_IMAGE =
|
const FALLBACK_IMAGE =
|
||||||
@@ -97,7 +99,7 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
const [progressUnit, setProgressUnit] = useState('');
|
const [progressUnit, setProgressUnit] = useState('');
|
||||||
const [periodLabel, setPeriodLabel] = useState('');
|
const [periodLabel, setPeriodLabel] = useState('');
|
||||||
const [periodEdited, setPeriodEdited] = useState(false);
|
const [periodEdited, setPeriodEdited] = useState(false);
|
||||||
const [rankingDescription] = useState('连续打卡榜');
|
const [rankingDescription] = useState(t('challenges.createCustom.rankingDescription'));
|
||||||
const [isPublic, setIsPublic] = useState(true);
|
const [isPublic, setIsPublic] = useState(true);
|
||||||
const [maxParticipants, setMaxParticipants] = useState('100');
|
const [maxParticipants, setMaxParticipants] = useState('100');
|
||||||
const [minimumEdited, setMinimumEdited] = useState(false);
|
const [minimumEdited, setMinimumEdited] = useState(false);
|
||||||
@@ -121,7 +123,6 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
setEndDate(new Date(challenge.endAt || Date.now()));
|
setEndDate(new Date(challenge.endAt || Date.now()));
|
||||||
setTargetValue(String(challenge.progress?.target || ''));
|
setTargetValue(String(challenge.progress?.target || ''));
|
||||||
setMinimumCheckInDays(String(challenge.minimumCheckInDays || ''));
|
setMinimumCheckInDays(String(challenge.minimumCheckInDays || ''));
|
||||||
setRequirementLabel(challenge.requirementLabel || '');
|
|
||||||
setSummary(challenge.summary || '');
|
setSummary(challenge.summary || '');
|
||||||
setProgressUnit(challenge.unit || '');
|
setProgressUnit(challenge.unit || '');
|
||||||
setPeriodLabel(challenge.periodLabel || '');
|
setPeriodLabel(challenge.periodLabel || '');
|
||||||
@@ -177,10 +178,6 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requirementLabel.trim()) {
|
|
||||||
Toast.warning(t('challenges.createCustom.alerts.requirementRequired'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTimestamp = dayjs(startDate).valueOf();
|
const startTimestamp = dayjs(startDate).valueOf();
|
||||||
const endTimestamp = dayjs(endDate).valueOf();
|
const endTimestamp = dayjs(endDate).valueOf();
|
||||||
@@ -221,7 +218,7 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
targetValue: target,
|
targetValue: target,
|
||||||
minimumCheckInDays: minDays,
|
minimumCheckInDays: minDays,
|
||||||
durationLabel,
|
durationLabel,
|
||||||
requirementLabel: requirementLabel.trim(),
|
requirementLabel: '',
|
||||||
summary: summary.trim() || undefined,
|
summary: summary.trim() || undefined,
|
||||||
progressUnit: progressUnit.trim(),
|
progressUnit: progressUnit.trim(),
|
||||||
periodLabel: periodLabel.trim() || undefined,
|
periodLabel: periodLabel.trim() || undefined,
|
||||||
@@ -513,16 +510,19 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={option.value}
|
key={option.value}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
onPress={() => setType(option.value)}
|
onPress={() => !isEditMode && setType(option.value)}
|
||||||
|
disabled={isEditMode}
|
||||||
style={[
|
style={[
|
||||||
styles.chip,
|
styles.chip,
|
||||||
active && { backgroundColor: `${option.accent}1A`, borderColor: option.accent },
|
active && { backgroundColor: `${option.accent}1A`, borderColor: option.accent },
|
||||||
|
isEditMode && styles.chipDisabled,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
styles.chipLabel,
|
styles.chipLabel,
|
||||||
active && { color: option.accent, fontWeight: '700' },
|
active && { color: option.accent, fontWeight: '700' },
|
||||||
|
isEditMode && styles.chipLabelDisabled,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -531,6 +531,7 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
<Text style={styles.helperText}>{t('challenges.createCustom.fields.challengeTypeHelper')}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.fieldBlock}>
|
<View style={styles.fieldBlock}>
|
||||||
@@ -592,7 +593,6 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
|
|
||||||
{renderField(t('challenges.createCustom.fields.minimumCheckInDays'), minimumCheckInDays, handleMinimumDaysChange, t('challenges.createCustom.fields.minimumCheckInDaysPlaceholder'), 'numeric')}
|
{renderField(t('challenges.createCustom.fields.minimumCheckInDays'), minimumCheckInDays, handleMinimumDaysChange, t('challenges.createCustom.fields.minimumCheckInDaysPlaceholder'), 'numeric')}
|
||||||
|
|
||||||
{renderField(t('challenges.createCustom.fields.challengeRequirement'), requirementLabel, setRequirementLabel, t('challenges.createCustom.fields.requirementPlaceholder'))}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.formCard}>
|
<View style={styles.formCard}>
|
||||||
@@ -675,6 +675,9 @@ export default function CreateCustomChallengeScreen() {
|
|||||||
minimumDate={pickerType === 'end' ? dayjs(startDate).add(1, 'day').toDate() : dayjs().add(1, 'day').toDate()}
|
minimumDate={pickerType === 'end' ? dayjs(startDate).add(1, 'day').toDate() : dayjs().add(1, 'day').toDate()}
|
||||||
onConfirm={handleConfirmDate}
|
onConfirm={handleConfirmDate}
|
||||||
onCancel={() => setPickerType(null)}
|
onCancel={() => setPickerType(null)}
|
||||||
|
locale={i18n.language}
|
||||||
|
confirmTextIOS={t('challenges.createCustom.datePicker.confirm')}
|
||||||
|
cancelTextIOS={t('challenges.createCustom.datePicker.cancel')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@@ -833,10 +836,17 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: '#e5e7eb',
|
||||||
},
|
},
|
||||||
|
chipDisabled: {
|
||||||
|
opacity: 0.5,
|
||||||
|
backgroundColor: '#f1f5f9',
|
||||||
|
},
|
||||||
chipLabel: {
|
chipLabel: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#334155',
|
color: '#334155',
|
||||||
},
|
},
|
||||||
|
chipLabelDisabled: {
|
||||||
|
color: '#94a3b8',
|
||||||
|
},
|
||||||
uploadRow: {
|
uploadRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -1210,10 +1210,12 @@ const resources = {
|
|||||||
sleep: '睡眠',
|
sleep: '睡眠',
|
||||||
mood: '心情',
|
mood: '心情',
|
||||||
weight: '体重',
|
weight: '体重',
|
||||||
|
custom: '自定义',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
title: '标题',
|
title: '标题',
|
||||||
titlePlaceholder: '挑战标题(最多100字)',
|
titlePlaceholder: '挑战标题(最多100字)',
|
||||||
|
challengeTypeHelper: '饮水、睡眠类型会触发自动上报进度,也可以手动标记完成',
|
||||||
coverImage: '封面图',
|
coverImage: '封面图',
|
||||||
uploadCover: '上传封面',
|
uploadCover: '上传封面',
|
||||||
challengeDescription: '挑战说明',
|
challengeDescription: '挑战说明',
|
||||||
@@ -1275,6 +1277,7 @@ const resources = {
|
|||||||
clear: '清除',
|
clear: '清除',
|
||||||
helper: '建议比例 16:9,清晰展示挑战氛围',
|
helper: '建议比例 16:9,清晰展示挑战氛围',
|
||||||
selectSource: '选择封面图',
|
selectSource: '选择封面图',
|
||||||
|
selectMessage: '请选择图片来源',
|
||||||
camera: '拍照',
|
camera: '拍照',
|
||||||
album: '从相册选择',
|
album: '从相册选择',
|
||||||
cancel: '取消',
|
cancel: '取消',
|
||||||
@@ -1288,6 +1291,11 @@ const resources = {
|
|||||||
selectFailed: '选择失败',
|
selectFailed: '选择失败',
|
||||||
selectFailedMessage: '无法打开相册,请稍后再试',
|
selectFailedMessage: '无法打开相册,请稍后再试',
|
||||||
},
|
},
|
||||||
|
rankingDescription: '连续打卡榜',
|
||||||
|
datePicker: {
|
||||||
|
confirm: '确认',
|
||||||
|
cancel: '取消',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2165,10 +2173,12 @@ const resources = {
|
|||||||
sleep: 'Sleep',
|
sleep: 'Sleep',
|
||||||
mood: 'Mood',
|
mood: 'Mood',
|
||||||
weight: 'Weight',
|
weight: 'Weight',
|
||||||
|
custom: 'Custom',
|
||||||
},
|
},
|
||||||
fields: {
|
fields: {
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
titlePlaceholder: 'Challenge title (max 100 characters)',
|
titlePlaceholder: 'Challenge title (max 100 characters)',
|
||||||
|
challengeTypeHelper: 'Water and Sleep types auto-report progress and can also be manually marked as complete',
|
||||||
coverImage: 'Cover Image',
|
coverImage: 'Cover Image',
|
||||||
uploadCover: 'Upload Cover',
|
uploadCover: 'Upload Cover',
|
||||||
challengeDescription: 'Challenge Description',
|
challengeDescription: 'Challenge Description',
|
||||||
@@ -2243,6 +2253,11 @@ const resources = {
|
|||||||
selectFailed: 'Selection Failed',
|
selectFailed: 'Selection Failed',
|
||||||
selectFailedMessage: 'Unable to open album, please try again later',
|
selectFailedMessage: 'Unable to open album, please try again later',
|
||||||
},
|
},
|
||||||
|
rankingDescription: 'Continuous Check-in Leaderboard',
|
||||||
|
datePicker: {
|
||||||
|
confirm: 'Confirm',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export enum ChallengeType {
|
|||||||
MOOD = 'mood',
|
MOOD = 'mood',
|
||||||
SLEEP = 'sleep',
|
SLEEP = 'sleep',
|
||||||
WEIGHT = 'weight',
|
WEIGHT = 'weight',
|
||||||
|
CUSTOM = 'custom',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user