feat: 优化目标创建成功提示及任务筛选功能
- 将目标创建成功后的提示从系统默认的 Alert.alert 改为使用自定义确认弹窗,提升用户体验和视觉一致性 - 在任务筛选中新增“已跳过”选项,支持用户更好地管理任务状态 - 更新任务卡片和进度卡片,展示跳过任务的数量和状态 - 调整相关组件样式,确保界面一致性和美观性 - 编写相关文档,详细描述新功能和使用方法
This commit is contained in:
@@ -2012,7 +2012,7 @@ export default function CoachScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<View style={[styles.inputRow, { borderColor: `${theme.primary}30`, backgroundColor: `${theme.primary}08` }]}>
|
<View style={[styles.inputRow]}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
onPress={pickImages}
|
onPress={pickImages}
|
||||||
@@ -2429,9 +2429,6 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 16,
|
|
||||||
backgroundColor: 'rgba(122,90,248,0.04)' // 使用紫色主题的极浅色背景
|
|
||||||
},
|
},
|
||||||
mediaBtn: {
|
mediaBtn: {
|
||||||
width: 40,
|
width: 40,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import CreateGoalModal from '@/components/CreateGoalModal';
|
|||||||
import { TaskCard } from '@/components/TaskCard';
|
import { TaskCard } from '@/components/TaskCard';
|
||||||
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
|
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
|
||||||
import { TaskProgressCard } from '@/components/TaskProgressCard';
|
import { TaskProgressCard } from '@/components/TaskProgressCard';
|
||||||
|
import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
||||||
@@ -21,6 +23,7 @@ export default function GoalsScreen() {
|
|||||||
const colorTokens = Colors[theme];
|
const colorTokens = Colors[theme];
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { showConfirm } = useGlobalDialog();
|
||||||
|
|
||||||
// Redux状态
|
// Redux状态
|
||||||
const {
|
const {
|
||||||
@@ -42,6 +45,7 @@ export default function GoalsScreen() {
|
|||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
|
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
|
||||||
|
const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗
|
||||||
|
|
||||||
// 页面聚焦时重新加载数据
|
// 页面聚焦时重新加载数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -109,12 +113,34 @@ export default function GoalsScreen() {
|
|||||||
}
|
}
|
||||||
}, [tasksError, createError, completeError, skipError, dispatch]);
|
}, [tasksError, createError, completeError, skipError, dispatch]);
|
||||||
|
|
||||||
|
// 重置弹窗表单数据
|
||||||
|
const handleModalSuccess = () => {
|
||||||
|
// 不需要在这里改变 modalKey,因为弹窗已经关闭了
|
||||||
|
// 下次打开时会自动使用新的 modalKey
|
||||||
|
};
|
||||||
|
|
||||||
// 创建目标处理函数
|
// 创建目标处理函数
|
||||||
const handleCreateGoal = async (goalData: CreateGoalRequest) => {
|
const handleCreateGoal = async (goalData: CreateGoalRequest) => {
|
||||||
try {
|
try {
|
||||||
await dispatch(createGoal(goalData)).unwrap();
|
await dispatch(createGoal(goalData)).unwrap();
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
Alert.alert('成功', '目标创建成功!');
|
|
||||||
|
// 使用确认弹窗显示成功消息
|
||||||
|
showConfirm(
|
||||||
|
{
|
||||||
|
title: '目标创建成功',
|
||||||
|
message: '恭喜!您的目标已成功创建。系统将自动生成相应的任务,帮助您实现目标。',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '',
|
||||||
|
icon: 'checkmark-circle',
|
||||||
|
iconColor: '#10B981',
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// 用户点击确定后的回调
|
||||||
|
console.log('用户确认了目标创建成功');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 创建目标后重新加载任务列表
|
// 创建目标后重新加载任务列表
|
||||||
loadTasks();
|
loadTasks();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,6 +160,7 @@ export default function GoalsScreen() {
|
|||||||
all: tasks.length,
|
all: tasks.length,
|
||||||
pending: tasks.filter(task => task.status === 'pending').length,
|
pending: tasks.filter(task => task.status === 'pending').length,
|
||||||
completed: tasks.filter(task => task.status === 'completed').length,
|
completed: tasks.filter(task => task.status === 'completed').length,
|
||||||
|
skipped: tasks.filter(task => task.status === 'skipped').length,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据筛选条件过滤任务
|
// 根据筛选条件过滤任务
|
||||||
@@ -143,6 +170,8 @@ export default function GoalsScreen() {
|
|||||||
return tasks.filter(task => task.status === 'pending');
|
return tasks.filter(task => task.status === 'pending');
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return tasks.filter(task => task.status === 'completed');
|
return tasks.filter(task => task.status === 'completed');
|
||||||
|
case 'skipped':
|
||||||
|
return tasks.filter(task => task.status === 'skipped');
|
||||||
default:
|
default:
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
@@ -171,6 +200,9 @@ export default function GoalsScreen() {
|
|||||||
} else if (selectedFilter === 'completed') {
|
} else if (selectedFilter === 'completed') {
|
||||||
title = '暂无已完成的任务';
|
title = '暂无已完成的任务';
|
||||||
subtitle = '完成一些任务后,它们会显示在这里';
|
subtitle = '完成一些任务后,它们会显示在这里';
|
||||||
|
} else if (selectedFilter === 'skipped') {
|
||||||
|
title = '暂无已跳过的任务';
|
||||||
|
subtitle = '跳过一些任务后,它们会显示在这里';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -262,7 +294,10 @@ export default function GoalsScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.cardAddButton, { backgroundColor: colorTokens.primary }]}
|
style={[styles.cardAddButton, { backgroundColor: colorTokens.primary }]}
|
||||||
onPress={() => setShowCreateModal(true)}
|
onPress={() => {
|
||||||
|
setModalKey(prev => prev + 1); // 每次打开弹窗时使用新的 key
|
||||||
|
setShowCreateModal(true);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text style={styles.cardAddButtonText}>+</Text>
|
<Text style={styles.cardAddButtonText}>+</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -303,9 +338,11 @@ export default function GoalsScreen() {
|
|||||||
|
|
||||||
{/* 创建目标弹窗 */}
|
{/* 创建目标弹窗 */}
|
||||||
<CreateGoalModal
|
<CreateGoalModal
|
||||||
|
key={modalKey}
|
||||||
visible={showCreateModal}
|
visible={showCreateModal}
|
||||||
onClose={() => setShowCreateModal(false)}
|
onClose={() => setShowCreateModal(false)}
|
||||||
onSubmit={handleCreateGoal}
|
onSubmit={handleCreateGoal}
|
||||||
|
onSuccess={handleModalSuccess}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -415,7 +452,7 @@ const styles = StyleSheet.create({
|
|||||||
taskList: {
|
taskList: {
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingTop: 20,
|
paddingTop: 20,
|
||||||
paddingBottom: 20,
|
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20, // 避让底部导航栏 + 额外间距
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ interface CreateGoalModalProps {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSubmit: (goalData: CreateGoalRequest) => void;
|
onSubmit: (goalData: CreateGoalRequest) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
|
|||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
onSuccess,
|
||||||
loading = false,
|
loading = false,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||||
@@ -65,7 +67,7 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
|
|||||||
setRepeatType('daily');
|
setRepeatType('daily');
|
||||||
setFrequency(1);
|
setFrequency(1);
|
||||||
setHasReminder(false);
|
setHasReminder(false);
|
||||||
setReminderTime('19:00');
|
setReminderTime('20:00');
|
||||||
setCategory('');
|
setCategory('');
|
||||||
setPriority(5);
|
setPriority(5);
|
||||||
};
|
};
|
||||||
@@ -113,6 +115,11 @@ export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
|
|||||||
console.log('goalData', goalData);
|
console.log('goalData', goalData);
|
||||||
|
|
||||||
onSubmit(goalData);
|
onSubmit(goalData);
|
||||||
|
|
||||||
|
// 通知父组件提交成功
|
||||||
|
if (onSuccess) {
|
||||||
|
onSuccess();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 时间选择器
|
// 时间选择器
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
showConfirm(
|
showConfirm(
|
||||||
{
|
{
|
||||||
title: '确认跳过任务',
|
title: '确认跳过任务',
|
||||||
message: `确定要跳过任务"${task.title}"吗?跳过后将无法恢复。`,
|
message: `确定要跳过任务"${task.title}"吗?\n\n跳过后的任务将不会显示在任务列表中,且无法恢复。`,
|
||||||
confirmText: '跳过',
|
confirmText: '跳过',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
destructive: true,
|
destructive: true,
|
||||||
@@ -189,11 +189,24 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
style={[
|
style={[
|
||||||
styles.progressFill,
|
styles.progressFill,
|
||||||
{
|
{
|
||||||
width: `${Math.min(task.progressPercentage, 100)}%`,
|
width: task.progressPercentage > 0 ? `${Math.min(task.progressPercentage, 100)}%` : '2%',
|
||||||
backgroundColor: colorTokens.primary,
|
backgroundColor: task.progressPercentage >= 100
|
||||||
|
? '#10B981'
|
||||||
|
: task.progressPercentage >= 50
|
||||||
|
? '#F59E0B'
|
||||||
|
: task.progressPercentage > 0
|
||||||
|
? colorTokens.primary
|
||||||
|
: '#E5E7EB',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{task.progressPercentage > 0 && task.progressPercentage < 100 && (
|
||||||
|
<View style={styles.progressGlow} />
|
||||||
|
)}
|
||||||
|
{/* 进度百分比文本 */}
|
||||||
|
<View style={styles.progressTextContainer}>
|
||||||
|
<Text style={styles.progressText}>{Math.round(task.progressPercentage)}%</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 底部信息 */}
|
{/* 底部信息 */}
|
||||||
@@ -314,15 +327,57 @@ const styles = StyleSheet.create({
|
|||||||
color: '#FFFFFF',
|
color: '#FFFFFF',
|
||||||
},
|
},
|
||||||
progressBar: {
|
progressBar: {
|
||||||
height: 2,
|
height: 6,
|
||||||
backgroundColor: '#E5E7EB',
|
backgroundColor: '#F3F4F6',
|
||||||
borderRadius: 1,
|
borderRadius: 3,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'visible',
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
position: 'relative',
|
||||||
},
|
},
|
||||||
progressFill: {
|
progressFill: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderRadius: 1,
|
borderRadius: 3,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
progressGlow: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 8,
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.6)',
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
progressTextContainer: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: -6,
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: 2,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#E5E7EB',
|
||||||
|
zIndex: 1,
|
||||||
|
},
|
||||||
|
progressText: {
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#374151',
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||||
|
|
||||||
export type TaskFilterType = 'all' | 'pending' | 'completed';
|
export type TaskFilterType = 'all' | 'pending' | 'completed' | 'skipped';
|
||||||
|
|
||||||
interface TaskFilterTabsProps {
|
interface TaskFilterTabsProps {
|
||||||
selectedFilter: TaskFilterType;
|
selectedFilter: TaskFilterType;
|
||||||
@@ -10,6 +10,7 @@ interface TaskFilterTabsProps {
|
|||||||
all: number;
|
all: number;
|
||||||
pending: number;
|
pending: number;
|
||||||
completed: number;
|
completed: number;
|
||||||
|
skipped: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +102,33 @@ export const TaskFilterTabs: React.FC<TaskFilterTabsProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 已跳过 Tab */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.tab,
|
||||||
|
selectedFilter === 'skipped' && styles.activeTab
|
||||||
|
]}
|
||||||
|
onPress={() => onFilterChange('skipped')}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.tabText,
|
||||||
|
selectedFilter === 'skipped' && styles.activeTabText
|
||||||
|
]}>
|
||||||
|
已跳过
|
||||||
|
</Text>
|
||||||
|
<View style={[
|
||||||
|
styles.badge,
|
||||||
|
selectedFilter === 'skipped' ? styles.activeBadge : styles.inactiveBadge
|
||||||
|
]}>
|
||||||
|
<Text style={[
|
||||||
|
styles.badgeText,
|
||||||
|
selectedFilter === 'skipped' && styles.activeBadgeText
|
||||||
|
]}>
|
||||||
|
{taskCounts.skipped}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
|
|||||||
{/* 已跳过 卡片 */}
|
{/* 已跳过 卡片 */}
|
||||||
<View style={styles.statusCard}>
|
<View style={styles.statusCard}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<MaterialIcons name="skip-next" size={16} color="#6B7280" />
|
<MaterialIcons name="skip-next" size={16} color="#F59E0B" />
|
||||||
<Text style={styles.cardLabel} numberOfLines={1}>已跳过</Text>
|
<Text style={styles.cardLabel} numberOfLines={1}>已跳过</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.cardCount}>{skippedTasks.length}</Text>
|
<Text style={styles.cardCount}>{skippedTasks.length}</Text>
|
||||||
@@ -106,7 +106,7 @@ const styles = StyleSheet.create({
|
|||||||
statusCards: {
|
statusCards: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
gap: 12,
|
gap: 8,
|
||||||
},
|
},
|
||||||
statusCard: {
|
statusCard: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -146,7 +146,11 @@ export function ConfirmDialog({
|
|||||||
{message && <Text style={styles.message}>{message}</Text>}
|
{message && <Text style={styles.message}>{message}</Text>}
|
||||||
|
|
||||||
{/* 按钮组 */}
|
{/* 按钮组 */}
|
||||||
<View style={styles.buttonContainer}>
|
<View style={[
|
||||||
|
styles.buttonContainer,
|
||||||
|
!cancelText && styles.singleButtonContainer
|
||||||
|
]}>
|
||||||
|
{cancelText && (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[styles.button, styles.cancelButton]}
|
style={[styles.button, styles.cancelButton]}
|
||||||
onPress={handleCancel}
|
onPress={handleCancel}
|
||||||
@@ -154,12 +158,14 @@ export function ConfirmDialog({
|
|||||||
>
|
>
|
||||||
<Text style={styles.cancelButtonText}>{cancelText}</Text>
|
<Text style={styles.cancelButtonText}>{cancelText}</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
styles.confirmButton,
|
styles.confirmButton,
|
||||||
{ backgroundColor: confirmButtonColor },
|
{ backgroundColor: confirmButtonColor },
|
||||||
|
!cancelText && styles.singleConfirmButton,
|
||||||
]}
|
]}
|
||||||
onPress={handleConfirm}
|
onPress={handleConfirm}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
@@ -226,12 +232,18 @@ const styles = StyleSheet.create({
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
|
singleButtonContainer: {
|
||||||
|
gap: 0,
|
||||||
|
},
|
||||||
button: {
|
button: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
singleConfirmButton: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
cancelButton: {
|
cancelButton: {
|
||||||
backgroundColor: '#F3F4F6',
|
backgroundColor: '#F3F4F6',
|
||||||
},
|
},
|
||||||
|
|||||||
175
docs/confirm-dialog-cancel-button-control.md
Normal file
175
docs/confirm-dialog-cancel-button-control.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# 确认弹窗取消按钮控制功能
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
ConfirmDialog 组件现在支持控制是否显示取消按钮,通过设置 `cancelText` 参数来控制。
|
||||||
|
|
||||||
|
## 实现原理
|
||||||
|
|
||||||
|
### 1. 条件渲染逻辑
|
||||||
|
|
||||||
|
在 ConfirmDialog 组件中,取消按钮的显示基于 `cancelText` 参数:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
{cancelText && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.button, styles.cancelButton]}
|
||||||
|
onPress={handleCancel}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={styles.cancelButtonText}>{cancelText}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 样式适配
|
||||||
|
|
||||||
|
根据是否显示取消按钮,动态调整按钮容器的样式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<View style={[
|
||||||
|
styles.buttonContainer,
|
||||||
|
!cancelText && styles.singleButtonContainer
|
||||||
|
]}>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 确认按钮样式
|
||||||
|
|
||||||
|
当只有确认按钮时,应用特殊样式:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.button,
|
||||||
|
styles.confirmButton,
|
||||||
|
{ backgroundColor: confirmButtonColor },
|
||||||
|
!cancelText && styles.singleConfirmButton,
|
||||||
|
]}
|
||||||
|
onPress={handleConfirm}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 显示双按钮(默认行为)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
showConfirm(
|
||||||
|
{
|
||||||
|
title: '确认操作',
|
||||||
|
message: '确定要执行此操作吗?',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '取消', // 设置取消按钮文本
|
||||||
|
icon: 'warning',
|
||||||
|
iconColor: '#F59E0B',
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// 确认回调
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 只显示确认按钮
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
showConfirm(
|
||||||
|
{
|
||||||
|
title: '目标创建成功',
|
||||||
|
message: '恭喜!您的目标已成功创建。',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '', // 空字符串或不设置
|
||||||
|
icon: 'checkmark-circle',
|
||||||
|
iconColor: '#10B981',
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// 确认回调
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完全不显示取消按钮
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
showConfirm(
|
||||||
|
{
|
||||||
|
title: '操作成功',
|
||||||
|
message: '操作已完成',
|
||||||
|
confirmText: '确定',
|
||||||
|
// 不设置 cancelText 或设置为 undefined
|
||||||
|
icon: 'checkmark-circle',
|
||||||
|
iconColor: '#10B981',
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// 确认回调
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 样式变化
|
||||||
|
|
||||||
|
### 双按钮布局
|
||||||
|
- 两个按钮并排显示
|
||||||
|
- 按钮之间有 12px 间距
|
||||||
|
- 每个按钮占据 50% 宽度
|
||||||
|
|
||||||
|
### 单按钮布局
|
||||||
|
- 只显示确认按钮
|
||||||
|
- 按钮占据 100% 宽度
|
||||||
|
- 无按钮间距
|
||||||
|
|
||||||
|
## 应用场景
|
||||||
|
|
||||||
|
### 1. 成功提示(单按钮)
|
||||||
|
- 目标创建成功
|
||||||
|
- 操作完成提示
|
||||||
|
- 信息确认
|
||||||
|
|
||||||
|
### 2. 确认操作(双按钮)
|
||||||
|
- 删除确认
|
||||||
|
- 跳过任务确认
|
||||||
|
- 重要操作确认
|
||||||
|
|
||||||
|
### 3. 警告提示(双按钮)
|
||||||
|
- 危险操作确认
|
||||||
|
- 数据丢失警告
|
||||||
|
- 权限确认
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 接口定义
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface DialogConfig {
|
||||||
|
title: string;
|
||||||
|
message?: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string; // 可选参数,控制取消按钮显示
|
||||||
|
destructive?: boolean;
|
||||||
|
icon?: keyof typeof Ionicons.glyphMap;
|
||||||
|
iconColor?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 样式类
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
buttonContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 12,
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
singleButtonContainer: {
|
||||||
|
gap: 0, // 单按钮时移除间距
|
||||||
|
},
|
||||||
|
singleConfirmButton: {
|
||||||
|
flex: 1, // 单按钮时占满宽度
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **空字符串 vs undefined**:两者都会隐藏取消按钮
|
||||||
|
2. **按钮文本**:建议为取消按钮提供有意义的文本
|
||||||
|
3. **用户体验**:单按钮适合成功提示,双按钮适合需要确认的操作
|
||||||
|
4. **无障碍访问**:确保按钮有足够的点击区域和清晰的文本
|
||||||
88
docs/goal-creation-success-dialog.md
Normal file
88
docs/goal-creation-success-dialog.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 目标创建成功弹窗优化
|
||||||
|
|
||||||
|
## 修改概述
|
||||||
|
|
||||||
|
将目标创建成功后的提示从系统默认的 `Alert.alert` 改为使用自定义的确认弹窗,提供更好的用户体验和视觉一致性。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
### 1. 导入依赖
|
||||||
|
|
||||||
|
在 `app/(tabs)/goals.tsx` 中添加了 `useGlobalDialog` hook 的导入:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useGlobalDialog } from '@/components/ui/DialogProvider';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用 Dialog Hook
|
||||||
|
|
||||||
|
在组件中添加了 `useGlobalDialog` hook 的使用:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { showConfirm } = useGlobalDialog();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 修改成功处理逻辑
|
||||||
|
|
||||||
|
将 `handleCreateGoal` 函数中的成功提示从:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
Alert.alert('成功', '目标创建成功!');
|
||||||
|
```
|
||||||
|
|
||||||
|
改为:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
showConfirm(
|
||||||
|
{
|
||||||
|
title: '目标创建成功',
|
||||||
|
message: '恭喜!您的目标已成功创建。系统将自动生成相应的任务,帮助您实现目标。',
|
||||||
|
confirmText: '确定',
|
||||||
|
cancelText: '',
|
||||||
|
icon: 'checkmark-circle',
|
||||||
|
iconColor: '#10B981',
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// 用户点击确定后的回调
|
||||||
|
console.log('用户确认了目标创建成功');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 改进效果
|
||||||
|
|
||||||
|
### 视觉一致性
|
||||||
|
- 使用与应用其他部分相同的弹窗样式
|
||||||
|
- 保持设计语言的一致性
|
||||||
|
|
||||||
|
### 用户体验提升
|
||||||
|
- 更友好的成功消息文案
|
||||||
|
- 使用绿色勾选图标表示成功状态
|
||||||
|
- 更详细的说明信息,告知用户系统将自动生成任务
|
||||||
|
|
||||||
|
### 交互优化
|
||||||
|
- 只显示"确定"按钮,简化用户操作
|
||||||
|
- 提供触觉反馈(通过 DialogProvider 实现)
|
||||||
|
- 流畅的动画效果
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 图标选择
|
||||||
|
- 使用 `checkmark-circle` 图标表示成功
|
||||||
|
- 图标颜色设置为 `#10B981`(绿色)
|
||||||
|
|
||||||
|
### 文案优化
|
||||||
|
- 标题:简洁明了
|
||||||
|
- 消息:详细说明后续流程
|
||||||
|
- 按钮:只保留"确定"按钮,避免用户困惑
|
||||||
|
|
||||||
|
### 回调处理
|
||||||
|
- 在用户确认后执行回调函数
|
||||||
|
- 可以在这里添加额外的逻辑处理
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 确保 `DialogProvider` 已正确配置在应用的根级别
|
||||||
|
2. 图标名称必须符合 Ionicons 的命名规范
|
||||||
|
3. 颜色值使用十六进制格式
|
||||||
|
4. 保持与其他弹窗使用的一致性
|
||||||
Reference in New Issue
Block a user