From 75806df6603d38227f2f426014cb4697734a45ff Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 22 Aug 2025 22:19:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=9B=AE=E6=A0=87?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=88=90=E5=8A=9F=E6=8F=90=E7=A4=BA=E5=8F=8A?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=AD=9B=E9=80=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将目标创建成功后的提示从系统默认的 Alert.alert 改为使用自定义确认弹窗,提升用户体验和视觉一致性 - 在任务筛选中新增“已跳过”选项,支持用户更好地管理任务状态 - 更新任务卡片和进度卡片,展示跳过任务的数量和状态 - 调整相关组件样式,确保界面一致性和美观性 - 编写相关文档,详细描述新功能和使用方法 --- app/(tabs)/coach.tsx | 5 +- app/(tabs)/goals.tsx | 43 ++++- components/CreateGoalModal.tsx | 29 +-- components/TaskCard.tsx | 71 +++++++- components/TaskFilterTabs.tsx | 30 +++- components/TaskProgressCard.tsx | 4 +- components/ui/ConfirmDialog.tsx | 28 ++- docs/confirm-dialog-cancel-button-control.md | 175 +++++++++++++++++++ docs/goal-creation-success-dialog.md | 88 ++++++++++ 9 files changed, 436 insertions(+), 37 deletions(-) create mode 100644 docs/confirm-dialog-cancel-button-control.md create mode 100644 docs/goal-creation-success-dialog.md diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx index aa1f4cc..e6356b8 100644 --- a/app/(tabs)/coach.tsx +++ b/app/(tabs)/coach.tsx @@ -2012,7 +2012,7 @@ export default function CoachScreen() { )} - + ('all'); + const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗 // 页面聚焦时重新加载数据 useFocusEffect( @@ -109,12 +113,34 @@ export default function GoalsScreen() { } }, [tasksError, createError, completeError, skipError, dispatch]); + // 重置弹窗表单数据 + const handleModalSuccess = () => { + // 不需要在这里改变 modalKey,因为弹窗已经关闭了 + // 下次打开时会自动使用新的 modalKey + }; + // 创建目标处理函数 const handleCreateGoal = async (goalData: CreateGoalRequest) => { try { await dispatch(createGoal(goalData)).unwrap(); setShowCreateModal(false); - Alert.alert('成功', '目标创建成功!'); + + // 使用确认弹窗显示成功消息 + showConfirm( + { + title: '目标创建成功', + message: '恭喜!您的目标已成功创建。系统将自动生成相应的任务,帮助您实现目标。', + confirmText: '确定', + cancelText: '', + icon: 'checkmark-circle', + iconColor: '#10B981', + }, + () => { + // 用户点击确定后的回调 + console.log('用户确认了目标创建成功'); + } + ); + // 创建目标后重新加载任务列表 loadTasks(); } catch (error) { @@ -134,6 +160,7 @@ export default function GoalsScreen() { all: tasks.length, pending: tasks.filter(task => task.status === 'pending').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'); case 'completed': return tasks.filter(task => task.status === 'completed'); + case 'skipped': + return tasks.filter(task => task.status === 'skipped'); default: return tasks; } @@ -171,6 +200,9 @@ export default function GoalsScreen() { } else if (selectedFilter === 'completed') { title = '暂无已完成的任务'; subtitle = '完成一些任务后,它们会显示在这里'; + } else if (selectedFilter === 'skipped') { + title = '暂无已跳过的任务'; + subtitle = '跳过一些任务后,它们会显示在这里'; } return ( @@ -262,7 +294,10 @@ export default function GoalsScreen() { setShowCreateModal(true)} + onPress={() => { + setModalKey(prev => prev + 1); // 每次打开弹窗时使用新的 key + setShowCreateModal(true); + }} > + @@ -303,9 +338,11 @@ export default function GoalsScreen() { {/* 创建目标弹窗 */} setShowCreateModal(false)} onSubmit={handleCreateGoal} + onSuccess={handleModalSuccess} loading={createLoading} /> @@ -415,7 +452,7 @@ const styles = StyleSheet.create({ taskList: { paddingHorizontal: 20, paddingTop: 20, - paddingBottom: 20, + paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20, // 避让底部导航栏 + 额外间距 }, emptyState: { alignItems: 'center', diff --git a/components/CreateGoalModal.tsx b/components/CreateGoalModal.tsx index 952a88e..f70b7c8 100644 --- a/components/CreateGoalModal.tsx +++ b/components/CreateGoalModal.tsx @@ -6,16 +6,16 @@ import DateTimePicker from '@react-native-community/datetimepicker'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useState } from 'react'; import { - Alert, - Modal, - Platform, - ScrollView, - StyleSheet, - Switch, - Text, - TextInput, - TouchableOpacity, - View, + Alert, + Modal, + Platform, + ScrollView, + StyleSheet, + Switch, + Text, + TextInput, + TouchableOpacity, + View, } from 'react-native'; import WheelPickerExpo from 'react-native-wheel-picker-expo'; @@ -23,6 +23,7 @@ interface CreateGoalModalProps { visible: boolean; onClose: () => void; onSubmit: (goalData: CreateGoalRequest) => void; + onSuccess?: () => void; loading?: boolean; } @@ -39,6 +40,7 @@ export const CreateGoalModal: React.FC = ({ visible, onClose, onSubmit, + onSuccess, loading = false, }) => { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; @@ -65,7 +67,7 @@ export const CreateGoalModal: React.FC = ({ setRepeatType('daily'); setFrequency(1); setHasReminder(false); - setReminderTime('19:00'); + setReminderTime('20:00'); setCategory(''); setPriority(5); }; @@ -113,6 +115,11 @@ export const CreateGoalModal: React.FC = ({ console.log('goalData', goalData); onSubmit(goalData); + + // 通知父组件提交成功 + if (onSuccess) { + onSuccess(); + } }; // 时间选择器 diff --git a/components/TaskCard.tsx b/components/TaskCard.tsx index 1686f4b..36ab748 100644 --- a/components/TaskCard.tsx +++ b/components/TaskCard.tsx @@ -100,7 +100,7 @@ export const TaskCard: React.FC = ({ showConfirm( { title: '确认跳过任务', - message: `确定要跳过任务"${task.title}"吗?跳过后将无法恢复。`, + message: `确定要跳过任务"${task.title}"吗?\n\n跳过后的任务将不会显示在任务列表中,且无法恢复。`, confirmText: '跳过', cancelText: '取消', destructive: true, @@ -189,11 +189,24 @@ export const TaskCard: React.FC = ({ style={[ styles.progressFill, { - width: `${Math.min(task.progressPercentage, 100)}%`, - backgroundColor: colorTokens.primary, + width: task.progressPercentage > 0 ? `${Math.min(task.progressPercentage, 100)}%` : '2%', + backgroundColor: task.progressPercentage >= 100 + ? '#10B981' + : task.progressPercentage >= 50 + ? '#F59E0B' + : task.progressPercentage > 0 + ? colorTokens.primary + : '#E5E7EB', }, ]} /> + {task.progressPercentage > 0 && task.progressPercentage < 100 && ( + + )} + {/* 进度百分比文本 */} + + {Math.round(task.progressPercentage)}% + {/* 底部信息 */} @@ -314,15 +327,57 @@ const styles = StyleSheet.create({ color: '#FFFFFF', }, progressBar: { - height: 2, - backgroundColor: '#E5E7EB', - borderRadius: 1, + height: 6, + backgroundColor: '#F3F4F6', + borderRadius: 3, marginBottom: 16, - overflow: 'hidden', + overflow: 'visible', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + elevation: 2, + position: 'relative', }, progressFill: { 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: { flexDirection: 'row', diff --git a/components/TaskFilterTabs.tsx b/components/TaskFilterTabs.tsx index 1cb278f..875523a 100644 --- a/components/TaskFilterTabs.tsx +++ b/components/TaskFilterTabs.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -export type TaskFilterType = 'all' | 'pending' | 'completed'; +export type TaskFilterType = 'all' | 'pending' | 'completed' | 'skipped'; interface TaskFilterTabsProps { selectedFilter: TaskFilterType; @@ -10,6 +10,7 @@ interface TaskFilterTabsProps { all: number; pending: number; completed: number; + skipped: number; }; } @@ -101,6 +102,33 @@ export const TaskFilterTabs: React.FC = ({ + + {/* 已跳过 Tab */} + onFilterChange('skipped')} + > + + 已跳过 + + + + {taskCounts.skipped} + + + ); diff --git a/components/TaskProgressCard.tsx b/components/TaskProgressCard.tsx index 647369d..92f5a94 100644 --- a/components/TaskProgressCard.tsx +++ b/components/TaskProgressCard.tsx @@ -55,7 +55,7 @@ export const TaskProgressCard: React.FC = ({ {/* 已跳过 卡片 */} - + 已跳过 {skippedTasks.length} @@ -106,7 +106,7 @@ const styles = StyleSheet.create({ statusCards: { flexDirection: 'row', justifyContent: 'space-between', - gap: 12, + gap: 8, }, statusCard: { flex: 1, diff --git a/components/ui/ConfirmDialog.tsx b/components/ui/ConfirmDialog.tsx index d147dcc..9af07cd 100644 --- a/components/ui/ConfirmDialog.tsx +++ b/components/ui/ConfirmDialog.tsx @@ -146,20 +146,26 @@ export function ConfirmDialog({ {message && {message}} {/* 按钮组 */} - - - {cancelText} - + + {cancelText && ( + + {cancelText} + + )} + {cancelText} + +)} +``` + +### 2. 样式适配 + +根据是否显示取消按钮,动态调整按钮容器的样式: + +```typescript + +``` + +### 3. 确认按钮样式 + +当只有确认按钮时,应用特殊样式: + +```typescript + +``` + +## 使用方法 + +### 显示双按钮(默认行为) + +```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. **无障碍访问**:确保按钮有足够的点击区域和清晰的文本 diff --git a/docs/goal-creation-success-dialog.md b/docs/goal-creation-success-dialog.md new file mode 100644 index 0000000..d0ac882 --- /dev/null +++ b/docs/goal-creation-success-dialog.md @@ -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. 保持与其他弹窗使用的一致性