feat(challenges): 实现自定义挑战的编辑与删除功能并完善多语言支持

- 新增自定义挑战的编辑模式,支持修改挑战信息
- 在详情页为创建者添加删除(归档)挑战的功能入口
- 全面完善挑战创建页面的国际化(i18n)文案适配
- 优化个人中心页面的字体样式,统一使用 AliBold/Regular
- 更新 Store 逻辑以处理挑战更新、删除及列表数据映射调整
This commit is contained in:
richarjiang
2025-11-26 19:07:19 +08:00
parent 39671ed70f
commit 518282ecb8
6 changed files with 866 additions and 160 deletions

View File

@@ -4,7 +4,7 @@ import * as Clipboard from 'expo-clipboard';
import { Image } from 'expo-image';
import * as ImagePicker from 'expo-image-picker';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Alert,
@@ -27,22 +27,32 @@ import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload';
import { ChallengeType, type CreateCustomChallengePayload } from '@/services/challengesApi';
import { useI18n } from '@/hooks/useI18n';
import {
ChallengeType,
type CreateCustomChallengePayload,
type UpdateCustomChallengePayload,
} from '@/services/challengesApi';
import { store } from '@/store';
import {
createCustomChallengeThunk,
fetchChallenges,
selectChallengeById,
selectCreateChallengeError,
selectCreateChallengeStatus,
selectUpdateChallengeError,
selectUpdateChallengeStatus,
updateCustomChallengeThunk
} from '@/store/challengesSlice';
import { Toast } from '@/utils/toast.utils';
const typeOptions: { value: ChallengeType; label: string; accent: string }[] = [
{ value: ChallengeType.WATER, label: '喝水', accent: '#5E8BFF' },
{ value: ChallengeType.EXERCISE, label: '运动', accent: '#6B6CFF' },
{ value: ChallengeType.DIET, label: '饮食', accent: '#38BDF8' },
{ value: ChallengeType.SLEEP, label: '睡眠', accent: '#7C3AED' },
{ value: ChallengeType.MOOD, label: '心情', accent: '#F97316' },
{ value: ChallengeType.WEIGHT, label: '体重', accent: '#22C55E' },
const getTypeOptions = (t: (key: string) => string): { value: ChallengeType; label: string; accent: string }[] => [
{ value: ChallengeType.WATER, label: t('challenges.createCustom.typeLabels.water'), accent: '#5E8BFF' },
{ value: ChallengeType.EXERCISE, label: t('challenges.createCustom.typeLabels.exercise'), accent: '#6B6CFF' },
{ value: ChallengeType.DIET, label: t('challenges.createCustom.typeLabels.diet'), accent: '#38BDF8' },
{ value: ChallengeType.SLEEP, label: t('challenges.createCustom.typeLabels.sleep'), accent: '#7C3AED' },
{ value: ChallengeType.MOOD, label: t('challenges.createCustom.typeLabels.mood'), accent: '#F97316' },
{ value: ChallengeType.WEIGHT, label: t('challenges.createCustom.typeLabels.weight'), accent: '#22C55E' },
];
const FALLBACK_IMAGE =
@@ -51,6 +61,9 @@ const FALLBACK_IMAGE =
type PickerType = 'start' | 'end' | null;
export default function CreateCustomChallengeScreen() {
const { id, mode } = useLocalSearchParams<{ id?: string; mode?: 'edit' }>();
const isEditMode = mode === 'edit' && !!id;
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const dispatch = useAppDispatch();
@@ -58,7 +71,14 @@ export default function CreateCustomChallengeScreen() {
const insets = useSafeAreaInsets();
const createStatus = useAppSelector(selectCreateChallengeStatus);
const createError = useAppSelector(selectCreateChallengeError);
const updateError = useAppSelector(selectUpdateChallengeError);
const updateStatus = useAppSelector(selectUpdateChallengeStatus);
const inlineError = isEditMode ? updateError : createError;
const isCreating = createStatus === 'loading';
const isUpdating = updateStatus === 'loading';
const { t } = useI18n();
const typeOptions = useMemo(() => getTypeOptions(t), [t]);
const today = useMemo(() => dayjs().startOf('day').toDate(), []);
const defaultEnd = useMemo(() => dayjs().add(21, 'day').startOf('day').toDate(), []);
@@ -74,7 +94,7 @@ export default function CreateCustomChallengeScreen() {
const [minimumCheckInDays, setMinimumCheckInDays] = useState('');
const [requirementLabel, setRequirementLabel] = useState('');
const [summary, setSummary] = useState('');
const [progressUnit] = useState('');
const [progressUnit, setProgressUnit] = useState('');
const [periodLabel, setPeriodLabel] = useState('');
const [periodEdited, setPeriodEdited] = useState(false);
const [rankingDescription] = useState('连续打卡榜');
@@ -88,6 +108,29 @@ export default function CreateCustomChallengeScreen() {
const [pickerType, setPickerType] = useState<PickerType>(null);
// 编辑模式下预填充数据
useEffect(() => {
if (isEditMode && id) {
const challengeSelector = selectChallengeById(id);
const challenge = challengeSelector(store.getState());
if (challenge) {
setTitle(challenge.title || '');
setImage(challenge.image);
setType(challenge.type);
setStartDate(new Date(challenge.startAt || Date.now()));
setEndDate(new Date(challenge.endAt || Date.now()));
setTargetValue(String(challenge.progress?.target || ''));
setMinimumCheckInDays(String(challenge.minimumCheckInDays || ''));
setRequirementLabel(challenge.requirementLabel || '');
setSummary(challenge.summary || '');
setProgressUnit(challenge.unit || '');
setPeriodLabel(challenge.periodLabel || '');
setIsPublic(challenge.isPublic ?? true);
setMaxParticipants(challenge.maxParticipants?.toString() || '100');
}
}
}, [isEditMode, id]);
const durationDays = useMemo(
() =>
Math.max(
@@ -96,16 +139,16 @@ export default function CreateCustomChallengeScreen() {
),
[startDate, endDate]
);
const durationLabel = useMemo(() => `持续${durationDays}`, [durationDays]);
const durationLabel = useMemo(() => t('challenges.createCustom.durationDays', { days: durationDays }), [durationDays, t]);
useEffect(() => {
if (!periodEdited) {
setPeriodLabel(`${durationDays}天挑战`);
setPeriodLabel(t('challenges.createCustom.durationDaysChallenge', { days: durationDays }));
}
if (!minimumEdited) {
setMinimumCheckInDays(String(durationDays));
}
}, [durationDays, minimumEdited, periodEdited]);
}, [durationDays, minimumEdited, periodEdited, t]);
const handleConfirmDate = (date: Date) => {
if (!pickerType) return;
@@ -128,47 +171,47 @@ export default function CreateCustomChallengeScreen() {
};
const handleSubmit = async () => {
if (isCreating) return;
if (isCreating || isUpdating) return;
if (!title.trim()) {
Toast.warning('请填写挑战标题');
Toast.warning(t('challenges.createCustom.alerts.titleRequired'));
return;
}
if (!requirementLabel.trim()) {
Toast.warning('请填写挑战要求说明');
if (!requirementLabel.trim()) {
Toast.warning(t('challenges.createCustom.alerts.requirementRequired'));
return;
}
const startTimestamp = dayjs(startDate).valueOf();
const endTimestamp = dayjs(endDate).valueOf();
if (endTimestamp <= startTimestamp) {
Toast.warning('结束时间需要晚于开始时间');
Toast.warning(t('challenges.createCustom.alerts.endTimeError'));
return;
}
const target = Number(targetValue);
if (!Number.isFinite(target) || target < 1 || target > 1000) {
Toast.warning('每日目标值需在 1-1000 之间');
Toast.warning(t('challenges.createCustom.alerts.targetValueError'));
return;
}
const minDays = Number(minimumCheckInDays) || durationDays;
if (!Number.isFinite(minDays) || minDays < 1 || minDays > 365) {
Toast.warning('最少打卡天数需在 1-365 之间');
Toast.warning(t('challenges.createCustom.alerts.minimumDaysError'));
return;
}
if (minDays > durationDays) {
Toast.warning('最少打卡天数不能超过持续天数');
Toast.warning(t('challenges.createCustom.alerts.minimumDaysExceedError'));
return;
}
const maxP = maxParticipants ? Number(maxParticipants) : null;
if (maxP !== null && (!Number.isFinite(maxP) || maxP < 2 || maxP > 10000)) {
Toast.warning('参与人数需在 2-10000 之间,或留空表示无限制');
Toast.warning(t('challenges.createCustom.alerts.participantsError'));
return;
}
const safeTitle = title.trim() || '自定义挑战';
const safeTitle = title.trim() || t('challenges.createCustom.defaultTitle');
const payload: CreateCustomChallengePayload = {
title: safeTitle,
type,
@@ -178,24 +221,39 @@ export default function CreateCustomChallengeScreen() {
targetValue: target,
minimumCheckInDays: minDays,
durationLabel,
requirementLabel: requirementLabel.trim() || '请填写挑战要求',
requirementLabel: requirementLabel.trim(),
summary: summary.trim() || undefined,
progressUnit: progressUnit.trim() || '天',
progressUnit: progressUnit.trim(),
periodLabel: periodLabel.trim() || undefined,
rankingDescription: rankingDescription.trim() || undefined,
isPublic,
maxParticipants: maxP,
};
const updatePayload: UpdateCustomChallengePayload = {
title: safeTitle,
image: image?.trim() || undefined,
summary: summary.trim() || undefined,
isPublic,
maxParticipants: maxP ?? undefined,
};
try {
if (isEditMode && id) {
await dispatch(updateCustomChallengeThunk({ id, payload: updatePayload })).unwrap();
Toast.success(t('challenges.createCustom.alerts.updateSuccess'));
dispatch(fetchChallenges());
return;
}
const created = await dispatch(createCustomChallengeThunk(payload)).unwrap();
setShareCode(created.shareCode ?? null);
setCreatedChallengeId(created.id);
setShareModalVisible(true);
Toast.success('自定义挑战已创建');
Toast.success(t('challenges.createCustom.alerts.createSuccess'));
dispatch(fetchChallenges());
} catch (error) {
const message = typeof error === 'string' ? error : '创建失败,请稍后再试';
const message = typeof error === 'string' ? error : t('challenges.createCustom.alerts.createFailed');
Toast.error(message);
}
};
@@ -203,7 +261,7 @@ export default function CreateCustomChallengeScreen() {
const handleCopyShareCode = async () => {
if (!shareCode) return;
await Clipboard.setStringAsync(shareCode);
Toast.success('邀请码已复制');
Toast.success(t('challenges.createCustom.shareModal.copyCode'));
};
const handleTargetInputChange = (value: string) => {
@@ -235,16 +293,16 @@ export default function CreateCustomChallengeScreen() {
const handlePickImage = useCallback(() => {
Alert.alert(
'选择封面图',
'请选择封面来源',
t('challenges.createCustom.imageUpload.selectSource'),
t('challenges.createCustom.imageUpload.selectMessage'),
[
{
text: '拍照',
text: t('challenges.createCustom.imageUpload.camera'),
onPress: async () => {
try {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (permission.status !== 'granted') {
Alert.alert('权限不足', '需要相机权限以拍摄封面');
Alert.alert(t('challenges.createCustom.imageUpload.cameraPermission'), t('challenges.createCustom.imageUpload.cameraPermissionMessage'));
return;
}
const result = await ImagePicker.launchCameraAsync({
@@ -269,21 +327,21 @@ export default function CreateCustomChallengeScreen() {
setImagePreview(null);
} catch (error) {
console.error('[CHALLENGE] 封面上传失败', error);
Alert.alert('上传失败', '封面上传失败,请稍后重试');
Alert.alert(t('challenges.createCustom.imageUpload.uploadFailed'), t('challenges.createCustom.imageUpload.uploadFailedMessage'));
}
} catch (error) {
console.error('[CHALLENGE] 拍照失败', error);
Alert.alert('拍照失败', '无法打开相机,请稍后再试');
Alert.alert(t('challenges.createCustom.imageUpload.cameraFailed'), t('challenges.createCustom.imageUpload.cameraFailedMessage'));
}
},
},
{
text: '从相册选择',
text: t('challenges.createCustom.imageUpload.album'),
onPress: async () => {
try {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permission.status !== 'granted') {
Alert.alert('权限不足', '需要相册权限以选择封面');
Alert.alert(t('challenges.createCustom.imageUpload.cameraPermission'), t('challenges.createCustom.imageUpload.albumPermissionMessage'));
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
@@ -307,19 +365,19 @@ export default function CreateCustomChallengeScreen() {
setImagePreview(null);
} catch (error) {
console.error('[CHALLENGE] 封面上传失败', error);
Alert.alert('上传失败', '封面上传失败,请稍后重试');
Alert.alert(t('challenges.createCustom.imageUpload.uploadFailed'), t('challenges.createCustom.imageUpload.uploadFailedMessage'));
}
} catch (error) {
console.error('[CHALLENGE] 选择封面失败', error);
Alert.alert('选择失败', '无法打开相册,请稍后再试');
Alert.alert(t('challenges.createCustom.imageUpload.selectFailed'), t('challenges.createCustom.imageUpload.selectFailedMessage'));
}
},
},
{ text: '取消', style: 'cancel' },
{ text: t('challenges.createCustom.imageUpload.cancel'), style: 'cancel' },
],
{ cancelable: true }
);
}, [upload]);
}, [upload, t]);
const handleViewChallenge = () => {
setShareModalVisible(false);
@@ -370,7 +428,7 @@ export default function CreateCustomChallengeScreen() {
</View>
);
const progressMeta = `${durationDays} · ${progressUnit || ''}`;
const progressMeta = `${durationDays} ${t('challenges.createCustom.dayUnit')}${progressUnit ? ` · ${progressUnit}` : ''}`;
const heroImageSource = imagePreview || image || FALLBACK_IMAGE;
return (
@@ -379,7 +437,7 @@ export default function CreateCustomChallengeScreen() {
colors={[colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd]}
style={StyleSheet.absoluteFillObject}
/>
<HeaderBar title="新建挑战" transparent />
<HeaderBar title={isEditMode ? t('challenges.createCustom.editTitle') : t('challenges.createCustom.title')} transparent />
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
@@ -403,20 +461,20 @@ export default function CreateCustomChallengeScreen() {
style={StyleSheet.absoluteFillObject}
/>
<View style={styles.heroOverlay}>
<Text style={styles.heroKicker}></Text>
<Text style={styles.heroTitle}>{title || '你的专属挑战'}</Text>
<Text style={styles.heroKicker}>{t('challenges.customChallenges')}</Text>
<Text style={styles.heroTitle}>{title || t('challenges.createCustom.yourChallenge')}</Text>
<Text style={styles.heroMeta}>{progressMeta}</Text>
</View>
</View>
<View style={styles.formCard}>
<View style={styles.formHeader}>
<Text style={styles.sectionTitle}></Text>
{createError ? <Text style={styles.inlineError}>{createError}</Text> : null}
<Text style={styles.sectionTitle}>{t('challenges.createCustom.basicInfo')}</Text>
{inlineError ? <Text style={styles.inlineError}>{inlineError}</Text> : null}
</View>
{renderField('标题', title, setTitle, '挑战标题最多100字')}
{renderField(t('challenges.createCustom.fields.title'), title, setTitle, t('challenges.createCustom.fields.titlePlaceholder'))}
<View style={styles.fieldBlock}>
<Text style={styles.fieldLabel}></Text>
<Text style={styles.fieldLabel}>{t('challenges.createCustom.fields.coverImage')}</Text>
<View style={styles.uploadRow}>
<TouchableOpacity
activeOpacity={0.9}
@@ -424,7 +482,7 @@ export default function CreateCustomChallengeScreen() {
onPress={handlePickImage}
disabled={uploading}
>
<Text style={styles.uploadButtonLabel}>{uploading ? '上传中…' : '上传封面'}</Text>
<Text style={styles.uploadButtonLabel}>{uploading ? t('challenges.createCustom.imageUpload.uploading') : t('challenges.createCustom.fields.uploadCover')}</Text>
</TouchableOpacity>
{image || imagePreview ? (
<TouchableOpacity
@@ -434,20 +492,20 @@ export default function CreateCustomChallengeScreen() {
setImage(undefined);
}}
>
<Text style={styles.clearUpload}></Text>
<Text style={styles.clearUpload}>{t('challenges.createCustom.imageUpload.clear')}</Text>
</TouchableOpacity>
) : null}
</View>
<Text style={styles.helperText}> 16:9</Text>
<Text style={styles.helperText}>{t('challenges.createCustom.imageUpload.helper')}</Text>
</View>
{renderTextarea('挑战说明', summary, setSummary, '简单介绍这个挑战的目标与要求')}
{renderTextarea(t('challenges.createCustom.fields.challengeDescription'), summary, setSummary, t('challenges.createCustom.fields.descriptionPlaceholder'))}
</View>
<View style={styles.formCard}>
<Text style={styles.sectionTitle}></Text>
<Text style={styles.sectionTitle}>{t('challenges.createCustom.challengeSettings')}</Text>
<View style={styles.fieldBlock}>
<Text style={styles.fieldLabel}></Text>
<Text style={styles.fieldLabel}>{t('challenges.createCustom.fields.challengeType')}</Text>
<View style={styles.chipRow}>
{typeOptions.map((option) => {
const active = option.value === type;
@@ -476,14 +534,14 @@ export default function CreateCustomChallengeScreen() {
</View>
<View style={styles.fieldBlock}>
<Text style={styles.fieldLabel}></Text>
<Text style={styles.fieldLabel}>{t('challenges.createCustom.fields.timeRange')}</Text>
<View style={styles.dateRow}>
<TouchableOpacity
activeOpacity={0.9}
style={styles.datePill}
onPress={() => setPickerType('start')}
>
<Text style={styles.dateLabel}></Text>
<Text style={styles.dateLabel}>{t('challenges.createCustom.fields.start')}</Text>
<Text style={styles.dateValue}>{dayjs(startDate).format('YYYY.MM.DD')}</Text>
</TouchableOpacity>
<TouchableOpacity
@@ -491,7 +549,7 @@ export default function CreateCustomChallengeScreen() {
style={styles.datePill}
onPress={() => setPickerType('end')}
>
<Text style={styles.dateLabel}></Text>
<Text style={styles.dateLabel}>{t('challenges.createCustom.fields.end')}</Text>
<Text style={styles.dateValue}>{dayjs(endDate).format('YYYY.MM.DD')}</Text>
</TouchableOpacity>
</View>
@@ -499,48 +557,60 @@ export default function CreateCustomChallengeScreen() {
<View style={styles.inlineFields}>
<View style={styles.fieldBlock}>
<Text style={styles.fieldLabel}></Text>
<Text style={styles.fieldLabel}>{t('challenges.createCustom.fields.duration')}</Text>
<View style={styles.readonlyPill}>
<Text style={styles.readonlyText}>{durationLabel}</Text>
</View>
</View>
{renderField('周期标签', periodLabel, (v) => {
{renderField(t('challenges.createCustom.fields.periodLabel'), periodLabel, (v) => {
setPeriodEdited(true);
setPeriodLabel(v);
}, '如21天挑战')}
}, t('challenges.createCustom.fields.periodLabelPlaceholder'))}
</View>
<View style={styles.inlineFields}>
{renderField('每日目标值', targetValue, handleTargetInputChange, '如8', 'numeric')}
<View style={styles.fieldBlock}>
<Text style={styles.fieldLabel}></Text>
<View style={styles.readonlyPill}>
<Text style={styles.readonlyText}>{progressUnit}</Text>
</View>
<View style={styles.fieldBlock}>
<Text style={styles.fieldLabel}>{t('challenges.createCustom.fields.dailyTargetAndUnit')}</Text>
<View style={styles.targetUnitRow}>
<TextInput
value={targetValue}
onChangeText={handleTargetInputChange}
placeholder={t('challenges.createCustom.fields.dailyTargetPlaceholder')}
placeholderTextColor="#9ca3af"
style={[styles.input, styles.targetInput]}
keyboardType="numeric"
/>
<TextInput
value={progressUnit}
onChangeText={setProgressUnit}
placeholder={t('challenges.createCustom.fields.unitPlaceholder')}
placeholderTextColor="#9ca3af"
style={[styles.input, styles.unitInput]}
/>
</View>
<Text style={styles.helperText}>{t('challenges.createCustom.fields.unitHelper')}</Text>
</View>
{renderField('最少打卡天数', minimumCheckInDays, handleMinimumDaysChange, '至少1天', 'numeric')}
{renderField(t('challenges.createCustom.fields.minimumCheckInDays'), minimumCheckInDays, handleMinimumDaysChange, t('challenges.createCustom.fields.minimumCheckInDaysPlaceholder'), 'numeric')}
{renderField('挑战要求说明', requirementLabel, setRequirementLabel, '例如:每日完成 30 分钟运动')}
{renderField(t('challenges.createCustom.fields.challengeRequirement'), requirementLabel, setRequirementLabel, t('challenges.createCustom.fields.requirementPlaceholder'))}
</View>
<View style={styles.formCard}>
<Text style={styles.sectionTitle}>&</Text>
<Text style={styles.sectionTitle}>{t('challenges.createCustom.displayInteraction')}</Text>
<View style={styles.inlineFields}>
{renderField('参与人数上限', maxParticipants, (v) => {
{renderField(t('challenges.createCustom.fields.maxParticipants'), maxParticipants, (v) => {
const digits = v.replace(/\D/g, '');
if (!digits) {
setMaxParticipants('');
return;
}
setMaxParticipants(String(parseInt(digits, 10)));
}, '留空表示无限制', 'numeric')}
}, t('challenges.createCustom.fields.noLimit'), 'numeric')}
</View>
<View style={styles.switchRow}>
<View>
<Text style={styles.fieldLabel}></Text>
<Text style={styles.switchHint}></Text>
<Text style={styles.fieldLabel}>{t('challenges.createCustom.fields.isPublic')}</Text>
<Text style={styles.switchHint}>{t('challenges.createCustom.fields.publicDescription')}</Text>
</View>
<Switch
value={isPublic}
@@ -557,14 +627,24 @@ export default function CreateCustomChallengeScreen() {
<BlurView intensity={14} tint="light" style={styles.floatingBlur}>
<View style={styles.floatingContent}>
<View style={styles.floatingCopy}>
<Text style={styles.floatingTitle}></Text>
<Text style={styles.floatingSubtitle}></Text>
<Text style={styles.floatingTitle}>
{isEditMode
? t('challenges.createCustom.floatingCTA.editTitle')
: t('challenges.createCustom.floatingCTA.title')
}
</Text>
<Text style={styles.floatingSubtitle}>
{isEditMode
? t('challenges.createCustom.floatingCTA.editSubtitle')
: t('challenges.createCustom.floatingCTA.subtitle')
}
</Text>
</View>
<TouchableOpacity
activeOpacity={0.9}
style={styles.floatingButton}
onPress={handleSubmit}
disabled={isCreating}
disabled={isCreating || isUpdating}
>
<LinearGradient
colors={['#5E8BFF', '#6B6CFF']}
@@ -573,7 +653,14 @@ export default function CreateCustomChallengeScreen() {
style={styles.floatingButtonBackground}
>
<Text style={styles.floatingButtonLabel}>
{isCreating ? '创建中…' : '创建并生成邀请码'}
{isCreating
? t('challenges.createCustom.buttons.creating')
: isUpdating
? t('challenges.createCustom.buttons.updating')
: isEditMode
? t('challenges.createCustom.buttons.updateAndSave')
: t('challenges.createCustom.buttons.createAndGenerateCode')
}
</Text>
</LinearGradient>
</TouchableOpacity>
@@ -598,10 +685,10 @@ export default function CreateCustomChallengeScreen() {
>
<View style={styles.modalOverlay}>
<View style={styles.shareCard}>
<Text style={styles.shareTitle}></Text>
<Text style={styles.shareSubtitle}></Text>
<Text style={styles.shareTitle}>{t('challenges.createCustom.shareModal.title')}</Text>
<Text style={styles.shareSubtitle}>{t('challenges.createCustom.shareModal.subtitle')}</Text>
<View style={styles.shareCodeBadge}>
<Text style={styles.shareCode}>{shareCode ?? '获取中…'}</Text>
<Text style={styles.shareCode}>{shareCode ?? t('challenges.createCustom.shareModal.generatingCode')}</Text>
</View>
<View style={styles.shareActions}>
<TouchableOpacity
@@ -610,7 +697,7 @@ export default function CreateCustomChallengeScreen() {
onPress={handleCopyShareCode}
disabled={!shareCode}
>
<Text style={styles.shareButtonGhostLabel}></Text>
<Text style={styles.shareButtonGhostLabel}>{t('challenges.createCustom.shareModal.copyCode')}</Text>
</TouchableOpacity>
<TouchableOpacity
activeOpacity={0.9}
@@ -623,7 +710,7 @@ export default function CreateCustomChallengeScreen() {
end={{ x: 1, y: 1 }}
style={styles.shareButtonPrimary}
>
<Text style={styles.shareButtonPrimaryLabel}></Text>
<Text style={styles.shareButtonPrimaryLabel}>{t('challenges.createCustom.shareModal.viewChallenge')}</Text>
</LinearGradient>
</TouchableOpacity>
</View>
@@ -632,7 +719,7 @@ export default function CreateCustomChallengeScreen() {
activeOpacity={0.8}
onPress={() => setShareModalVisible(false)}
>
<Text style={styles.shareCloseLabel}></Text>
<Text style={styles.shareCloseLabel}>{t('challenges.createCustom.shareModal.later')}</Text>
</TouchableOpacity>
</View>
</View>
@@ -720,6 +807,16 @@ const styles = StyleSheet.create({
fontSize: 15,
color: '#111827',
},
targetUnitRow: {
flexDirection: 'row',
gap: 12,
},
targetInput: {
flex: 1,
},
unitInput: {
flex: 1,
},
textarea: {
minHeight: 90,
},