154b79c8f1
1. 新增完整短链接工具服务,包括前端页面、后端API与数据库支持 2. 新增移动端日历提醒工具与鉴权配置 3. 优化日历事件编辑与展示: - 添加事件循环重复设置 - 新增全天事件支持 - 时间选择器自动对齐15分钟间隔 - 保存状态加载动画 - 事件卡片显示重复规则 4. 更新API基础地址与路由配置 5. 新增SSL证书文件与相关工具静态资源
184 lines
4.7 KiB
TypeScript
184 lines
4.7 KiB
TypeScript
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;
|
|
}
|
|
|
|
const WEEKDAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
|
|
|
// 生成循环规则中文描述
|
|
const getRecurrenceLabel = (recurrence: string | null): string | null => {
|
|
if (!recurrence) return null;
|
|
const parts = recurrence.split(';');
|
|
const type = parts[0];
|
|
let interval = 1;
|
|
let by: number[] = [];
|
|
for (let i = 1; i < parts.length; i++) {
|
|
const [key, val] = parts[i].split('=');
|
|
if (key === 'interval') interval = parseInt(val) || 1;
|
|
if (key === 'by') by = val.split(',').map(Number);
|
|
}
|
|
if (type === 'DAILY') {
|
|
return interval === 1 ? '每天' : `每${interval}天`;
|
|
}
|
|
if (type === 'WEEKLY') {
|
|
const prefix = interval === 1 ? '每周' : `每${interval}周`;
|
|
if (by.length > 0) {
|
|
return `${prefix} ${by.map(d => WEEKDAY_NAMES[d] || '').join('、')}`;
|
|
}
|
|
return prefix;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
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',
|
|
});
|
|
};
|
|
|
|
const recurrenceLabel = getRecurrenceLabel(event.recurrence);
|
|
|
|
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>
|
|
{recurrenceLabel && (
|
|
<>
|
|
<Ionicons
|
|
name="repeat-outline"
|
|
size={14}
|
|
color={theme.colors.primary}
|
|
style={{ marginLeft: 8 }}
|
|
/>
|
|
<Text style={styles.recurrenceText}>{recurrenceLabel}</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,
|
|
},
|
|
recurrenceText: {
|
|
...theme.typography.caption,
|
|
color: theme.colors.primary,
|
|
},
|
|
deleteButton: {
|
|
padding: theme.spacing.sm,
|
|
},
|
|
});
|