feat: 新增任务管理功能及相关组件
- 将目标页面改为任务列表,支持任务的创建、完成和跳过功能 - 新增任务卡片和任务进度卡片组件,展示任务状态和进度 - 实现任务数据的获取和管理,集成Redux状态管理 - 更新API服务,支持任务相关的CRUD操作 - 编写任务管理功能实现文档,详细描述功能和组件架构
This commit is contained in:
222
components/DateSelector.tsx
Normal file
222
components/DateSelector.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
export interface DateSelectorProps {
|
||||
/** 当前选中的日期索引 */
|
||||
selectedIndex?: number;
|
||||
/** 日期选择回调 */
|
||||
onDateSelect?: (index: number, date: Date) => void;
|
||||
/** 是否显示月份标题 */
|
||||
showMonthTitle?: boolean;
|
||||
/** 自定义月份标题 */
|
||||
monthTitle?: string;
|
||||
/** 是否禁用未来日期 */
|
||||
disableFutureDates?: boolean;
|
||||
/** 自定义样式 */
|
||||
style?: any;
|
||||
/** 容器样式 */
|
||||
containerStyle?: any;
|
||||
/** 日期项样式 */
|
||||
dayItemStyle?: any;
|
||||
/** 是否自动滚动到选中项 */
|
||||
autoScrollToSelected?: boolean;
|
||||
}
|
||||
|
||||
export const DateSelector: React.FC<DateSelectorProps> = ({
|
||||
selectedIndex: externalSelectedIndex,
|
||||
onDateSelect,
|
||||
showMonthTitle = true,
|
||||
monthTitle: externalMonthTitle,
|
||||
disableFutureDates = true,
|
||||
style,
|
||||
containerStyle,
|
||||
dayItemStyle,
|
||||
autoScrollToSelected = true,
|
||||
}) => {
|
||||
// 内部状态管理
|
||||
const [internalSelectedIndex, setInternalSelectedIndex] = useState(getTodayIndexInMonth());
|
||||
const selectedIndex = externalSelectedIndex ?? internalSelectedIndex;
|
||||
|
||||
// 获取日期数据
|
||||
const days = getMonthDaysZh();
|
||||
const monthTitle = externalMonthTitle ?? getMonthTitleZh();
|
||||
|
||||
// 滚动相关
|
||||
const daysScrollRef = useRef<ScrollView | null>(null);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const DAY_PILL_WIDTH = 48;
|
||||
const DAY_PILL_SPACING = 8;
|
||||
|
||||
// 滚动到指定索引
|
||||
const scrollToIndex = (index: number, animated = true) => {
|
||||
if (!daysScrollRef.current || scrollWidth === 0) return;
|
||||
|
||||
const itemWidth = DAY_PILL_WIDTH + DAY_PILL_SPACING;
|
||||
const baseOffset = index * itemWidth;
|
||||
const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2));
|
||||
|
||||
// 确保不会滚动超出边界
|
||||
const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth);
|
||||
const finalOffset = Math.min(centerOffset, maxScrollOffset);
|
||||
|
||||
daysScrollRef.current.scrollTo({ x: finalOffset, animated });
|
||||
};
|
||||
|
||||
// 初始化时滚动到选中项
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0 && autoScrollToSelected) {
|
||||
scrollToIndex(selectedIndex, false);
|
||||
}
|
||||
}, [scrollWidth, selectedIndex, autoScrollToSelected]);
|
||||
|
||||
// 当选中索引变化时,滚动到对应位置
|
||||
useEffect(() => {
|
||||
if (scrollWidth > 0 && autoScrollToSelected) {
|
||||
scrollToIndex(selectedIndex, true);
|
||||
}
|
||||
}, [selectedIndex, autoScrollToSelected]);
|
||||
|
||||
// 处理日期选择
|
||||
const handleDateSelect = (index: number) => {
|
||||
const targetDate = days[index]?.date?.toDate();
|
||||
if (!targetDate) return;
|
||||
|
||||
// 检查是否为未来日期
|
||||
if (disableFutureDates && days[index].date.isAfter(dayjs(), 'day')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新内部状态(如果使用外部控制则不更新)
|
||||
if (externalSelectedIndex === undefined) {
|
||||
setInternalSelectedIndex(index);
|
||||
}
|
||||
|
||||
// 调用回调
|
||||
onDateSelect?.(index, targetDate);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
{showMonthTitle && (
|
||||
<Text style={styles.monthTitle}>{monthTitle}</Text>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.daysContainer}
|
||||
ref={daysScrollRef}
|
||||
onLayout={(e) => setScrollWidth(e.nativeEvent.layout.width)}
|
||||
style={style}
|
||||
>
|
||||
{days.map((d, i) => {
|
||||
const selected = i === selectedIndex;
|
||||
const isFutureDate = disableFutureDates && d.date.isAfter(dayjs(), 'day');
|
||||
|
||||
return (
|
||||
<View key={`${d.dayOfMonth}`} style={[styles.dayItemWrapper, dayItemStyle]}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.dayPill,
|
||||
selected ? styles.dayPillSelected : styles.dayPillNormal,
|
||||
isFutureDate && styles.dayPillDisabled
|
||||
]}
|
||||
onPress={() => !isFutureDate && handleDateSelect(i)}
|
||||
activeOpacity={isFutureDate ? 1 : 0.8}
|
||||
disabled={isFutureDate}
|
||||
>
|
||||
<Text style={[
|
||||
styles.dayLabel,
|
||||
selected && styles.dayLabelSelected,
|
||||
isFutureDate && styles.dayLabelDisabled
|
||||
]}>
|
||||
{d.weekdayZh}
|
||||
</Text>
|
||||
<Text style={[
|
||||
styles.dayDate,
|
||||
selected && styles.dayDateSelected,
|
||||
isFutureDate && styles.dayDateDisabled
|
||||
]}>
|
||||
{d.dayOfMonth}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
monthTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
color: '#192126',
|
||||
marginBottom: 14,
|
||||
},
|
||||
daysContainer: {
|
||||
paddingBottom: 8,
|
||||
},
|
||||
dayItemWrapper: {
|
||||
alignItems: 'center',
|
||||
width: 48,
|
||||
marginRight: 8,
|
||||
},
|
||||
dayPill: {
|
||||
width: 40,
|
||||
height: 60,
|
||||
borderRadius: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dayPillNormal: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
dayPillSelected: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
dayPillDisabled: {
|
||||
backgroundColor: 'transparent',
|
||||
opacity: 0.4,
|
||||
},
|
||||
dayLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
color: 'gray',
|
||||
marginBottom: 2,
|
||||
},
|
||||
dayLabelSelected: {
|
||||
color: '#192126',
|
||||
},
|
||||
dayLabelDisabled: {
|
||||
color: 'gray',
|
||||
},
|
||||
dayDate: {
|
||||
fontSize: 12,
|
||||
fontWeight: '800',
|
||||
color: 'gray',
|
||||
},
|
||||
dayDateSelected: {
|
||||
color: '#192126',
|
||||
},
|
||||
dayDateDisabled: {
|
||||
color: 'gray',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user