feat: 初始提交(仅核心代码,已排除大文件)

This commit is contained in:
yangxiangyuan
2026-07-08 10:29:50 +08:00
commit be7c5842f1
623 changed files with 332897 additions and 0 deletions
@@ -0,0 +1,139 @@
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { theme } from '../theme';
import { Event } from '../types';
import { safeParseDate } from '../utils/recurrence';
interface EventCardProps {
event: Event;
onEdit: (event: Event) => void;
onToggleComplete: (event: Event) => void;
onDelete: (event: Event) => void;
}
export default function EventCard({
event,
onEdit,
onToggleComplete,
onDelete,
}: EventCardProps) {
const formatTime = (dateString: string) => {
const date = safeParseDate(dateString);
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
});
};
return (
<TouchableOpacity
style={[styles.container, event.completed && styles.completedContainer]}
onPress={() => onEdit(event)}
activeOpacity={0.7}
>
<TouchableOpacity
style={styles.checkbox}
onPress={() => onToggleComplete(event)}
>
{event.completed ? (
<Ionicons name="checkmark-circle" size={28} color={theme.colors.primary} />
) : (
<View style={styles.checkboxEmpty} />
)}
</TouchableOpacity>
<View style={styles.content}>
<Text
style={[
styles.title,
event.completed && styles.completedText,
]}
numberOfLines={2}
>
{event.title}
</Text>
{event.content ? (
<Text
style={[
styles.contentText,
event.completed && styles.completedText,
]}
numberOfLines={2}
>
{event.content}
</Text>
) : null}
<View style={styles.timeContainer}>
<Ionicons
name="time-outline"
size={16}
color={theme.colors.textSecondary}
/>
<Text style={styles.timeText}>{formatTime(event.start)}</Text>
</View>
</View>
<TouchableOpacity
style={styles.deleteButton}
onPress={() => onDelete(event)}
>
<Ionicons name="trash-outline" size={20} color={theme.colors.error} />
</TouchableOpacity>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: theme.colors.surface,
borderRadius: theme.borderRadius.lg,
padding: theme.spacing.md,
marginBottom: theme.spacing.sm,
alignItems: 'center',
borderWidth: 1,
borderColor: theme.colors.surfaceVariant,
},
completedContainer: {
opacity: 0.6,
},
checkbox: {
marginRight: theme.spacing.sm,
},
checkboxEmpty: {
width: 28,
height: 28,
borderRadius: 14,
borderWidth: 2,
borderColor: theme.colors.textSecondary,
},
content: {
flex: 1,
},
title: {
...theme.typography.h3,
color: theme.colors.text,
marginBottom: theme.spacing.xs,
},
contentText: {
...theme.typography.bodySmall,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
completedText: {
textDecorationLine: 'line-through',
},
timeContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
},
timeText: {
...theme.typography.caption,
color: theme.colors.textSecondary,
},
deleteButton: {
padding: theme.spacing.sm,
},
});
@@ -0,0 +1,374 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
Modal,
TextInput,
TouchableOpacity,
ScrollView,
Switch,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import DateTimePicker from '@react-native-community/datetimepicker';
import { theme } from '../theme';
import { Event } from '../types';
import { safeParseDate } from '../utils/recurrence';
interface EventModalProps {
visible: boolean;
event: Event | null;
onClose: () => void;
onSave: (event: Partial<Event>) => void;
}
export default function EventModal({
visible,
event,
onClose,
onSave,
}: EventModalProps) {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [startDate, setStartDate] = useState(new Date());
const [endDate, setEndDate] = useState<Date | null>(null);
const [enableReminder, setEnableReminder] = useState(true);
const [remindDate, setRemindDate] = useState(new Date());
const [recurrenceType, setRecurrenceType] = useState<'NONE' | 'DAILY' | 'WEEKLY'>('NONE');
const [showStartPicker, setShowStartPicker] = useState(false);
const [showEndPicker, setShowEndPicker] = useState(false);
const [showRemindPicker, setShowRemindPicker] = useState(false);
useEffect(() => {
if (event) {
setTitle(event.title);
setContent(event.content || '');
setStartDate(safeParseDate(event.start));
setEndDate(event.end ? safeParseDate(event.end) : null);
setEnableReminder(!!event.remindAt);
setRemindDate(event.remindAt ? safeParseDate(event.remindAt) : new Date());
if (event.recurrence?.startsWith('DAILY')) {
setRecurrenceType('DAILY');
} else if (event.recurrence?.startsWith('WEEKLY')) {
setRecurrenceType('WEEKLY');
} else {
setRecurrenceType('NONE');
}
} else {
resetForm();
}
}, [event, visible]);
const resetForm = () => {
setTitle('');
setContent('');
setStartDate(new Date());
setEndDate(null);
setEnableReminder(true);
setRemindDate(new Date());
setRecurrenceType('NONE');
};
const handleSave = () => {
if (!title.trim()) {
alert('请输入标题');
return;
}
let recurrence = '';
if (recurrenceType === 'DAILY') {
recurrence = 'DAILY;interval=1';
} else if (recurrenceType === 'WEEKLY') {
recurrence = 'WEEKLY;interval=1';
}
const eventData: Partial<Event> = {
title: title.trim(),
content: content.trim(),
start: startDate.toISOString(),
end: endDate ? endDate.toISOString() : null,
allDay: false,
remindAt: enableReminder ? remindDate.toISOString() : null,
recurrence: recurrence || null,
completed: event?.completed || false,
};
onSave(eventData);
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
return (
<Modal
visible={visible}
animationType="slide"
transparent={true}
onRequestClose={onClose}
>
<View style={styles.overlay}>
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose}>
<Ionicons name="close" size={28} color={theme.colors.text} />
</TouchableOpacity>
<Text style={styles.headerTitle}>
{event ? '编辑事件' : '新建事件'}
</Text>
<TouchableOpacity onPress={handleSave}>
<Text style={styles.saveButton}>保存</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
<View style={styles.field}>
<Text style={styles.label}>标题</Text>
<TextInput
style={styles.input}
value={title}
onChangeText={setTitle}
placeholder="输入事件标题"
placeholderTextColor={theme.colors.textSecondary}
/>
</View>
<View style={styles.field}>
<Text style={styles.label}>内容</Text>
<TextInput
style={[styles.input, styles.textArea]}
value={content}
onChangeText={setContent}
placeholder="输入事件内容(可选)"
placeholderTextColor={theme.colors.textSecondary}
multiline
numberOfLines={4}
/>
</View>
<View style={styles.field}>
<Text style={styles.label}>开始时间</Text>
<TouchableOpacity
style={styles.dateButton}
onPress={() => setShowStartPicker(true)}
>
<Text style={styles.dateButtonText}>{formatDate(startDate)}</Text>
</TouchableOpacity>
</View>
<View style={styles.field}>
<Text style={styles.label}>结束时间(可选)</Text>
<TouchableOpacity
style={styles.dateButton}
onPress={() => setShowEndPicker(true)}
>
<Text style={styles.dateButtonText}>
{endDate ? formatDate(endDate) : '未设置'}
</Text>
{endDate && (
<TouchableOpacity
onPress={() => setEndDate(null)}
style={styles.clearButton}
>
<Ionicons name="close-circle" size={20} color={theme.colors.textSecondary} />
</TouchableOpacity>
)}
</TouchableOpacity>
</View>
<View style={styles.switchField}>
<Text style={styles.label}>启用提醒</Text>
<Switch
value={enableReminder}
onValueChange={setEnableReminder}
trackColor={{ false: theme.colors.border, true: theme.colors.primary }}
thumbColor={theme.colors.text}
/>
</View>
{enableReminder && (
<View style={styles.field}>
<Text style={styles.label}>提醒时间</Text>
<TouchableOpacity
style={styles.dateButton}
onPress={() => setShowRemindPicker(true)}
>
<Text style={styles.dateButtonText}>{formatDate(remindDate)}</Text>
</TouchableOpacity>
</View>
)}
<View style={styles.field}>
<Text style={styles.label}>重复</Text>
<View style={styles.recurrenceOptions}>
{[
{ label: '不重复', value: 'NONE' },
{ label: '每天', value: 'DAILY' },
{ label: '每周', value: 'WEEKLY' },
].map((option) => (
<TouchableOpacity
key={option.value}
style={[
styles.recurrenceButton,
recurrenceType === option.value && styles.recurrenceButtonActive,
]}
onPress={() => setRecurrenceType(option.value as any)}
>
<Text
style={[
styles.recurrenceButtonText,
recurrenceType === option.value && styles.recurrenceButtonTextActive,
]}
>
{option.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</ScrollView>
{showStartPicker && (
<DateTimePicker
value={startDate}
mode="datetime"
onChange={(event, date) => {
setShowStartPicker(false);
if (date) setStartDate(date);
}}
/>
)}
{showEndPicker && (
<DateTimePicker
value={endDate || new Date()}
mode="datetime"
onChange={(event, date) => {
setShowEndPicker(false);
if (date) setEndDate(date);
}}
/>
)}
{showRemindPicker && (
<DateTimePicker
value={remindDate}
mode="datetime"
onChange={(event, date) => {
setShowRemindPicker(false);
if (date) setRemindDate(date);
}}
/>
)}
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
container: {
backgroundColor: theme.colors.background,
borderTopLeftRadius: theme.borderRadius.xl,
borderTopRightRadius: theme.borderRadius.xl,
maxHeight: '90%',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: theme.spacing.md,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
headerTitle: {
...theme.typography.h3,
color: theme.colors.text,
},
saveButton: {
...theme.typography.body,
color: theme.colors.primary,
fontWeight: '600',
},
content: {
padding: theme.spacing.md,
},
field: {
marginBottom: theme.spacing.lg,
},
label: {
...theme.typography.body,
color: theme.colors.text,
marginBottom: theme.spacing.sm,
},
input: {
backgroundColor: theme.colors.surface,
borderRadius: theme.borderRadius.md,
padding: theme.spacing.md,
color: theme.colors.text,
fontSize: 16,
},
textArea: {
height: 100,
textAlignVertical: 'top',
},
dateButton: {
backgroundColor: theme.colors.surface,
borderRadius: theme.borderRadius.md,
padding: theme.spacing.md,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
dateButtonText: {
...theme.typography.body,
color: theme.colors.text,
},
clearButton: {
marginLeft: theme.spacing.sm,
},
switchField: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: theme.spacing.lg,
},
recurrenceOptions: {
flexDirection: 'row',
gap: theme.spacing.sm,
},
recurrenceButton: {
flex: 1,
paddingVertical: theme.spacing.sm,
paddingHorizontal: theme.spacing.md,
backgroundColor: theme.colors.surface,
borderRadius: theme.borderRadius.md,
alignItems: 'center',
borderWidth: 1,
borderColor: theme.colors.border,
},
recurrenceButtonActive: {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.surfaceVariant,
},
recurrenceButtonText: {
...theme.typography.bodySmall,
color: theme.colors.text,
},
recurrenceButtonTextActive: {
color: theme.colors.primary,
fontWeight: '600',
},
});