feat: 完善目标管理功能及相关组件
- 新增创建目标弹窗,支持用户输入目标信息并提交 - 实现目标数据的转换,支持将目标转换为待办事项和时间轴事件 - 优化目标页面,集成Redux状态管理,处理目标的创建、完成和错误提示 - 更新时间轴组件,支持动态显示目标安排 - 编写目标管理功能实现文档,详细描述功能和组件架构
This commit is contained in:
523
components/CreateGoalModal.tsx
Normal file
523
components/CreateGoalModal.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { CreateGoalRequest, GoalPriority, RepeatType } from '@/types/goals';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Modal,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
interface CreateGoalModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (goalData: CreateGoalRequest) => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const REPEAT_TYPE_OPTIONS: { value: RepeatType; label: string }[] = [
|
||||
{ value: 'daily', label: '每日' },
|
||||
{ value: 'weekly', label: '每周' },
|
||||
{ value: 'monthly', label: '每月' },
|
||||
{ value: 'custom', label: '自定义' },
|
||||
];
|
||||
|
||||
const FREQUENCY_OPTIONS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
export const CreateGoalModal: React.FC<CreateGoalModalProps> = ({
|
||||
visible,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading = false,
|
||||
}) => {
|
||||
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
// 表单状态
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [repeatType, setRepeatType] = useState<RepeatType>('daily');
|
||||
const [frequency, setFrequency] = useState(1);
|
||||
const [hasReminder, setHasReminder] = useState(false);
|
||||
const [reminderTime, setReminderTime] = useState('20:00');
|
||||
const [category, setCategory] = useState('');
|
||||
const [priority, setPriority] = useState<GoalPriority>(5);
|
||||
const [showTimePicker, setShowTimePicker] = useState(false);
|
||||
const [tempSelectedTime, setTempSelectedTime] = useState<Date | null>(null);
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setRepeatType('daily');
|
||||
setFrequency(1);
|
||||
setHasReminder(false);
|
||||
setReminderTime('19:00');
|
||||
setCategory('');
|
||||
setPriority(5);
|
||||
};
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = () => {
|
||||
if (!loading) {
|
||||
resetForm();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理提交
|
||||
const handleSubmit = () => {
|
||||
if (!title.trim()) {
|
||||
Alert.alert('提示', '请输入目标标题');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算startTime:从reminderTime中获取小时和分钟,转换为当天的分钟数
|
||||
let startTime: number | undefined;
|
||||
if (reminderTime) {
|
||||
const [hours, minutes] = reminderTime.split(':').map(Number);
|
||||
startTime = hours * 60 + minutes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const goalData: CreateGoalRequest = {
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
repeatType,
|
||||
frequency,
|
||||
category: category.trim() || undefined,
|
||||
priority,
|
||||
hasReminder,
|
||||
reminderTime: hasReminder ? reminderTime : undefined,
|
||||
reminderSettings: hasReminder ? {
|
||||
enabled: true,
|
||||
weekdays: [1, 2, 3, 4, 5, 6, 0], // 默认每天
|
||||
} : undefined,
|
||||
startTime,
|
||||
};
|
||||
|
||||
console.log('goalData', goalData);
|
||||
|
||||
onSubmit(goalData);
|
||||
};
|
||||
|
||||
// 时间选择器
|
||||
const handleTimeChange = (event: any, selectedDate?: Date) => {
|
||||
if (Platform.OS === 'android') {
|
||||
// Android: 用户点击系统确认按钮后自动关闭
|
||||
if (event.type === 'set' && selectedDate) {
|
||||
const hours = selectedDate.getHours().toString().padStart(2, '0');
|
||||
const minutes = selectedDate.getMinutes().toString().padStart(2, '0');
|
||||
setReminderTime(`${hours}:${minutes}`);
|
||||
}
|
||||
setShowTimePicker(false);
|
||||
} else {
|
||||
// iOS: 只在用户点击自定义确认按钮时更新
|
||||
if (selectedDate) {
|
||||
setTempSelectedTime(selectedDate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmTime = () => {
|
||||
setShowTimePicker(false);
|
||||
if (tempSelectedTime) {
|
||||
const hours = tempSelectedTime.getHours().toString().padStart(2, '0');
|
||||
const minutes = tempSelectedTime.getMinutes().toString().padStart(2, '0');
|
||||
setReminderTime(`${hours}:${minutes}`);
|
||||
}
|
||||
setTempSelectedTime(null);
|
||||
};
|
||||
|
||||
const handleCancelTime = () => {
|
||||
setShowTimePicker(false);
|
||||
setTempSelectedTime(null);
|
||||
};
|
||||
|
||||
const showTimePickerModal = () => {
|
||||
setShowTimePicker(true);
|
||||
};
|
||||
|
||||
// 获取当前时间对应的Date对象
|
||||
const getCurrentTimeDate = () => {
|
||||
const [hours, minutes] = reminderTime.split(':').map(Number);
|
||||
const date = new Date();
|
||||
date.setHours(hours, minutes, 0, 0);
|
||||
return date;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<View style={[styles.container, { backgroundColor: colorTokens.background }]}>
|
||||
{/* 头部 */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={handleClose} disabled={loading}>
|
||||
<Text style={[styles.cancelButton, { color: colorTokens.text }]}>
|
||||
←
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]}>
|
||||
创建新目标
|
||||
</Text>
|
||||
<View style={{ width: 24 }} />
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
{/* 目标标题输入 */}
|
||||
<View style={styles.section}>
|
||||
<View style={styles.iconTitleContainer}>
|
||||
<View style={styles.iconPlaceholder}>
|
||||
<Text style={styles.iconText}>图标</Text>
|
||||
</View>
|
||||
<TextInput
|
||||
style={[styles.titleInput, { color: colorTokens.text }]}
|
||||
placeholder="写点什么..."
|
||||
placeholderTextColor={colorTokens.textSecondary}
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
multiline
|
||||
maxLength={100}
|
||||
/>
|
||||
</View>
|
||||
{/* 装饰图案 */}
|
||||
<View style={styles.decorationContainer}>
|
||||
<View style={styles.decoration} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 目标重复周期 */}
|
||||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||||
<View style={styles.optionHeader}>
|
||||
<View style={styles.optionIcon}>
|
||||
<Text style={styles.optionIconText}>🔄</Text>
|
||||
</View>
|
||||
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
|
||||
目标重复周期
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.optionValue}>
|
||||
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
|
||||
{REPEAT_TYPE_OPTIONS.find(opt => opt.value === repeatType)?.label}
|
||||
</Text>
|
||||
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
|
||||
›
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 频率设置 */}
|
||||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||||
<View style={styles.optionHeader}>
|
||||
<View style={styles.optionIcon}>
|
||||
<Text style={styles.optionIconText}>📊</Text>
|
||||
</View>
|
||||
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
|
||||
频率
|
||||
</Text>
|
||||
<TouchableOpacity style={styles.optionValue}>
|
||||
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
|
||||
{frequency}
|
||||
</Text>
|
||||
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
|
||||
›
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 提醒设置 */}
|
||||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||||
<View style={styles.optionHeader}>
|
||||
<View style={styles.optionIcon}>
|
||||
<Text style={styles.optionIconText}>🔔</Text>
|
||||
</View>
|
||||
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
|
||||
提醒
|
||||
</Text>
|
||||
<Switch
|
||||
value={hasReminder}
|
||||
onValueChange={setHasReminder}
|
||||
trackColor={{ false: '#E5E5E5', true: '#6366F1' }}
|
||||
thumbColor={hasReminder ? '#FFFFFF' : '#FFFFFF'}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 时间设置 */}
|
||||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||||
<View style={styles.optionHeader}>
|
||||
<View style={styles.optionIcon}>
|
||||
<Text style={styles.optionIconText}>⏰</Text>
|
||||
</View>
|
||||
<Text style={[styles.optionLabel, { color: colorTokens.text }]}>
|
||||
时间
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.optionValue]}
|
||||
onPress={showTimePickerModal}
|
||||
>
|
||||
<Text style={[styles.optionValueText, { color: colorTokens.textSecondary }]}>
|
||||
{reminderTime}
|
||||
</Text>
|
||||
<Text style={[styles.chevron, { color: colorTokens.textSecondary }]}>
|
||||
›
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 时间选择器弹窗 */}
|
||||
<Modal
|
||||
visible={showTimePicker}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowTimePicker(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.modalBackdrop}
|
||||
activeOpacity={1}
|
||||
onPress={handleCancelTime}
|
||||
/>
|
||||
<View style={styles.modalSheet}>
|
||||
<DateTimePicker
|
||||
value={tempSelectedTime || getCurrentTimeDate()}
|
||||
mode="time"
|
||||
is24Hour={true}
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
onChange={handleTimeChange}
|
||||
/>
|
||||
{Platform.OS === 'ios' && (
|
||||
<View style={styles.modalActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtn]}
|
||||
onPress={handleCancelTime}
|
||||
>
|
||||
<Text style={styles.modalBtnText}>取消</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.modalBtn, styles.modalBtnPrimary]}
|
||||
onPress={handleConfirmTime}
|
||||
>
|
||||
<Text style={[styles.modalBtnText, styles.modalBtnTextPrimary]}>确定</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* 描述输入(可选) */}
|
||||
<View style={[styles.optionCard, { backgroundColor: colorTokens.card }]}>
|
||||
<TextInput
|
||||
style={[styles.descriptionInput, { color: colorTokens.text }]}
|
||||
placeholder="添加描述(可选)"
|
||||
placeholderTextColor={colorTokens.textSecondary}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
multiline
|
||||
maxLength={500}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.saveButton,
|
||||
{ opacity: loading || !title.trim() ? 0.5 : 1 }
|
||||
]}
|
||||
onPress={handleSubmit}
|
||||
disabled={loading || !title.trim()}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>
|
||||
{loading ? '保存中...' : '保存'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 60,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
cancelButton: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
section: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
iconTitleContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginBottom: 16,
|
||||
},
|
||||
iconPlaceholder: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 16,
|
||||
},
|
||||
iconText: {
|
||||
fontSize: 12,
|
||||
color: '#9CA3AF',
|
||||
fontWeight: '500',
|
||||
},
|
||||
titleInput: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
minHeight: 60,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
decorationContainer: {
|
||||
alignItems: 'flex-end',
|
||||
paddingRight: 20,
|
||||
},
|
||||
decoration: {
|
||||
width: 80,
|
||||
height: 60,
|
||||
backgroundColor: '#E0E7FF',
|
||||
borderRadius: 40,
|
||||
opacity: 0.6,
|
||||
},
|
||||
optionCard: {
|
||||
borderRadius: 16,
|
||||
marginBottom: 12,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
optionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
optionIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
optionIconText: {
|
||||
fontSize: 16,
|
||||
},
|
||||
optionLabel: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
optionValue: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
optionValueText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
marginRight: 8,
|
||||
},
|
||||
chevron: {
|
||||
fontSize: 20,
|
||||
fontWeight: '300',
|
||||
},
|
||||
descriptionInput: {
|
||||
padding: 16,
|
||||
fontSize: 16,
|
||||
minHeight: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
footer: {
|
||||
padding: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
saveButton: {
|
||||
backgroundColor: '#6366F1',
|
||||
borderRadius: 16,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
saveButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalBackdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||
},
|
||||
modalSheet: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
padding: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
},
|
||||
modalActions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: 8,
|
||||
gap: 12,
|
||||
},
|
||||
modalBtn: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#F1F5F9',
|
||||
},
|
||||
modalBtnPrimary: {
|
||||
backgroundColor: '#6366F1',
|
||||
},
|
||||
modalBtnText: {
|
||||
color: '#334155',
|
||||
fontWeight: '700',
|
||||
},
|
||||
modalBtnTextPrimary: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
export default CreateGoalModal;
|
||||
@@ -73,7 +73,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
tabContainer: {
|
||||
flexDirection: 'row',
|
||||
borderRadius: 12,
|
||||
borderRadius: 20,
|
||||
padding: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
@@ -87,7 +87,7 @@ const styles = StyleSheet.create({
|
||||
tab: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 32,
|
||||
borderRadius: 16,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
@@ -44,7 +44,8 @@ const getEventStyle = (event: TimelineEvent) => {
|
||||
const endMinutes = endTime.hour() * 60 + endTime.minute();
|
||||
const durationMinutes = endMinutes - startMinutes;
|
||||
|
||||
const top = (startMinutes / 60) * HOUR_HEIGHT;
|
||||
// 计算top位置时需要加上时间标签的paddingTop偏移(8px)
|
||||
const top = (startMinutes / 60) * HOUR_HEIGHT + 8;
|
||||
const height = Math.max((durationMinutes / 60) * HOUR_HEIGHT, 30); // 最小高度30
|
||||
|
||||
return { top, height };
|
||||
@@ -93,9 +94,13 @@ export function TimelineSchedule({ events, selectedDate, onEventPress }: Timelin
|
||||
const categoryColor = getCategoryColor(event.category);
|
||||
|
||||
// 计算水平偏移和宽度,用于处理重叠事件
|
||||
const eventWidth = (screenWidth - TIME_LABEL_WIDTH - 40) / Math.max(groupSize, 1);
|
||||
const availableWidth = screenWidth - TIME_LABEL_WIDTH - 48; // 减少一些边距
|
||||
const eventWidth = availableWidth / Math.max(groupSize, 1);
|
||||
const leftOffset = index * eventWidth;
|
||||
|
||||
// 判断是否应该显示时间段 - 当卡片高度小于50或宽度小于80时隐藏时间段
|
||||
const shouldShowTimeRange = height >= 50 && eventWidth >= 80;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={event.id}
|
||||
@@ -104,8 +109,8 @@ export function TimelineSchedule({ events, selectedDate, onEventPress }: Timelin
|
||||
{
|
||||
top,
|
||||
height,
|
||||
left: TIME_LABEL_WIDTH + 20 + leftOffset,
|
||||
width: eventWidth - 4,
|
||||
left: TIME_LABEL_WIDTH + 24 + leftOffset, // 调整左偏移对齐
|
||||
width: eventWidth - 8, // 增加卡片间距
|
||||
backgroundColor: event.isCompleted
|
||||
? `${categoryColor}40`
|
||||
: `${categoryColor}80`,
|
||||
@@ -124,15 +129,17 @@ export function TimelineSchedule({ events, selectedDate, onEventPress }: Timelin
|
||||
textDecorationLine: event.isCompleted ? 'line-through' : 'none'
|
||||
}
|
||||
]}
|
||||
numberOfLines={2}
|
||||
numberOfLines={shouldShowTimeRange ? 1 : 2} // 当显示时间时,标题只显示1行
|
||||
>
|
||||
{event.title}
|
||||
</Text>
|
||||
|
||||
<Text style={[styles.eventTime, { color: colorTokens.textSecondary }]}>
|
||||
{dayjs(event.startTime).format('HH:mm')}
|
||||
{event.endTime && ` - ${dayjs(event.endTime).format('HH:mm')}`}
|
||||
</Text>
|
||||
{shouldShowTimeRange && (
|
||||
<Text style={[styles.eventTime, { color: colorTokens.textSecondary }]}>
|
||||
{dayjs(event.startTime).format('HH:mm')}
|
||||
{event.endTime && ` - ${dayjs(event.endTime).format('HH:mm')}`}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{event.isCompleted && (
|
||||
<View style={styles.completedIcon}>
|
||||
@@ -146,22 +153,23 @@ export function TimelineSchedule({ events, selectedDate, onEventPress }: Timelin
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 日期标题 */}
|
||||
<View style={styles.dateHeader}>
|
||||
<Text style={[styles.dateText, { color: colorTokens.text }]}>
|
||||
{dayjs(selectedDate).format('YYYY年M月D日 dddd')}
|
||||
</Text>
|
||||
<Text style={[styles.eventCount, { color: colorTokens.textSecondary }]}>
|
||||
{events.length} 项任务
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 时间轴 */}
|
||||
<ScrollView
|
||||
style={styles.timelineContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{ paddingBottom: 50 }}
|
||||
>
|
||||
{/* 日期标题 */}
|
||||
<View style={styles.dateHeader}>
|
||||
<Text style={[styles.dateText, { color: colorTokens.text }]}>
|
||||
{dayjs(selectedDate).format('YYYY年M月D日 dddd')}
|
||||
</Text>
|
||||
<Text style={[styles.eventCount, { color: colorTokens.textSecondary }]}>
|
||||
{events.length} 个目标
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
||||
<View style={styles.timeline}>
|
||||
{/* 时间标签 */}
|
||||
<View style={styles.timeLabelsContainer}>
|
||||
@@ -220,7 +228,8 @@ function CurrentTimeLine() {
|
||||
|
||||
const now = dayjs();
|
||||
const currentMinutes = now.hour() * 60 + now.minute();
|
||||
const top = (currentMinutes / 60) * HOUR_HEIGHT;
|
||||
// 当前时间线也需要加上时间标签的paddingTop偏移(8px)
|
||||
const top = (currentMinutes / 60) * HOUR_HEIGHT + 8;
|
||||
|
||||
return (
|
||||
<View
|
||||
@@ -245,16 +254,14 @@ const styles = StyleSheet.create({
|
||||
dateHeader: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
marginBottom: 4,
|
||||
},
|
||||
eventCount: {
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
},
|
||||
timelineContainer: {
|
||||
@@ -283,7 +290,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
borderBottomWidth: 1,
|
||||
marginLeft: 8,
|
||||
marginRight: 20,
|
||||
marginRight: 24,
|
||||
},
|
||||
eventsContainer: {
|
||||
position: 'absolute',
|
||||
@@ -309,6 +316,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
padding: 8,
|
||||
justifyContent: 'space-between',
|
||||
|
||||
},
|
||||
eventTitle: {
|
||||
fontSize: 12,
|
||||
@@ -327,8 +335,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
currentTimeLine: {
|
||||
position: 'absolute',
|
||||
left: TIME_LABEL_WIDTH + 20,
|
||||
right: 20,
|
||||
left: TIME_LABEL_WIDTH + 24,
|
||||
right: 24,
|
||||
height: 2,
|
||||
zIndex: 10,
|
||||
},
|
||||
|
||||
@@ -95,9 +95,7 @@ export function TodoCard({ item, onPress, onToggleComplete }: TodoCardProps) {
|
||||
<Text style={styles.categoryText}>{item.category}</Text>
|
||||
</View>
|
||||
|
||||
{item.priority && (
|
||||
<View style={[styles.priorityDot, { backgroundColor: priorityColor }]} />
|
||||
)}
|
||||
<Ionicons name="star-outline" size={18} color={colorTokens.textMuted} />
|
||||
</View>
|
||||
|
||||
{/* 主要内容 */}
|
||||
@@ -117,11 +115,11 @@ export function TodoCard({ item, onPress, onToggleComplete }: TodoCardProps) {
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.timeContainer}>
|
||||
<Ionicons
|
||||
name={isToday ? "time" : "calendar-outline"}
|
||||
name='time-outline'
|
||||
size={14}
|
||||
color={colorTokens.textMuted}
|
||||
/>
|
||||
<Text style={[styles.timeText, { color: colorTokens.textMuted }]}>
|
||||
<Text style={[styles.timeText]}>
|
||||
{timeFormatted}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -159,13 +157,6 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
position: 'relative',
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ export function TodoCarousel({ todos, onTodoPress, onToggleComplete }: TodoCarou
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部指示器 */}
|
||||
<View style={styles.indicatorContainer}>
|
||||
{/* <View style={styles.indicatorContainer}>
|
||||
{todos.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
@@ -62,7 +62,7 @@ export function TodoCarousel({ todos, onTodoPress, onToggleComplete }: TodoCarou
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -71,7 +71,6 @@ const styles = StyleSheet.create({
|
||||
container: {
|
||||
},
|
||||
scrollView: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 20,
|
||||
|
||||
Reference in New Issue
Block a user