日期范围选择组件

This commit is contained in:
2025-09-25 00:10:24 +08:00
parent dd1136d1e6
commit d99d3d87a9
4 changed files with 374 additions and 258 deletions

View File

@@ -6,7 +6,7 @@ import { PickerCommon, PickerCommonRef } from '@/components/Picker'
import dayjs from 'dayjs'
import styles from './index.module.scss'
export interface DialogCalendarCardProps {
value?: Date
value?: Date | Date[]
searchType?: 'single' | 'range' | 'multiple'
onChange?: (date: Date | Date[]) => void
visible: boolean
@@ -22,7 +22,7 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
value,
onChange,
}) => {
const [selected, setSelected] = useState<Date>(value || new Date())
const [selected, setSelected] = useState<Date | Date[]>(value || new Date())
const calendarRef = useRef<CalendarUIRef>(null);
const [type, setType] = useState<'year' | 'month' | 'time'>('year');
const [selectedHour, setSelectedHour] = useState(8)
@@ -32,6 +32,11 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
const [pendingJump, setPendingJump] = useState<{ year: number; month: number } | null>(null)
const handleConfirm = () => {
if (type === 'year') {
if (searchType === 'range') {
if (onChange) onChange(selected);
onClose();
return;
}
// 年份选择完成后,进入月份选择
setType('time')
} else if (type === 'month') {
@@ -40,7 +45,13 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
if (value) {
const year = value[0] as number
const month = value[1] as number
if (searchType === 'range') {
const delta = calculateMonthDifference(selected as Date, new Date(year, month - 1, 1))
console.log('xxxxx', calendarRef.current)
calendarRef.current?.gotoMonth(delta)
} else {
setSelected(new Date(year, month - 1, 1))
}
setPendingJump({ year, month })
}
setType('year')
@@ -54,18 +65,31 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
setSelectedMinute(minute)
const hours = hour.toString().padStart(2, '0')
const minutes = minute.toString().padStart(2, '0')
const finalDate = new Date(dayjs(selected).format('YYYY-MM-DD') + ' ' + hours + ':' + minutes)
const finalDate = new Date(dayjs(selected as Date).format('YYYY-MM-DD') + ' ' + hours + ':' + minutes)
if (onChange) onChange(finalDate)
}
onClose()
}
}
const calculateMonthDifference = (date1, date2) => {
if (!(date1 instanceof Date) || !(date2 instanceof Date)) {
throw new Error('Both arguments must be Date objects');
}
let months = (date2.getFullYear() - date1.getFullYear()) * 12;
months -= date1.getMonth();
months += date2.getMonth();
return months;
}
const handleChange = (d: Date | Date[]) => {
console.log('handleChange', d)
if (searchType === 'range') {
if (Array.isArray(d)) {
if (d.length === 2) {
onChange?.(d as Date[])
setSelected(d as Date[])
return
}
}
@@ -77,6 +101,7 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
}
}
const onHeaderClick = (date: Date) => {
console.log('onHeaderClick', date)
setSelected(date)
setType('month')
}
@@ -101,8 +126,8 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
useEffect(() => {
if (visible && value) {
setSelected(value || new Date())
setSelectedHour(value ? dayjs(value).hour() : 8)
setSelectedMinute(value ? dayjs(value).minute() : 0)
setSelectedHour(value ? dayjs(value as Date).hour() : 8)
setSelectedMinute(value ? dayjs(value as Date).minute() : 0)
}
}, [value, visible])
@@ -146,7 +171,7 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
ref={pickerRef}
onChange={handleDateTimePickerChange}
type="month"
value={[selected.getFullYear(), selected.getMonth() + 1]}
value={[(selected as Date).getFullYear(), (selected as Date).getMonth() + 1]}
/>
}

View File

@@ -5,6 +5,7 @@ import images from '@/config/images'
import styles from './index.module.scss'
import { getMonth, getWeekend, getWeekendOfCurrentWeek } from '@/utils/timeUtils'
import { PopupPicker } from '@/components/Picker/index'
import dayjs from 'dayjs'
interface NutUICalendarProps {
type?: 'single' | 'range' | 'multiple'
value?: string | Date | String[] | Date[]
@@ -16,7 +17,8 @@ interface NutUICalendarProps {
}
export interface CalendarUIRef {
jumpTo: (year: number, month: number) => void
jumpTo: (year: number, month: number) => void,
gotoMonth: (delta: number) => void,
}
const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
@@ -28,27 +30,27 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
onHeaderClick
}, ref) => {
// 根据类型初始化选中值
// const getInitialValue = (): Date | Date[] => {
// console.log(value,defaultValue,'today')
// const getInitialValue = (): Date | Date[] => {
// console.log(value,defaultValue,'today')
// if (typeof value === 'string' && value) {
// return new Date(value)
// }
// if (Array.isArray(value) && value.length > 0) {
// return value.map(item => new Date(item))
// }
// if (typeof defaultValue === 'string' && defaultValue) {
// return new Date(defaultValue)
// }
// if (Array.isArray(defaultValue) && defaultValue.length > 0) {
// return defaultValue.map(item => new Date(item))
// }
// const today = new Date();
// if (type === 'multiple') {
// return [today]
// }
// return today
// }
// if (typeof value === 'string' && value) {
// return new Date(value)
// }
// if (Array.isArray(value) && value.length > 0) {
// return value.map(item => new Date(item))
// }
// if (typeof defaultValue === 'string' && defaultValue) {
// return new Date(defaultValue)
// }
// if (Array.isArray(defaultValue) && defaultValue.length > 0) {
// return defaultValue.map(item => new Date(item))
// }
// const today = new Date();
// if (type === 'multiple') {
// return [today]
// }
// return today
// }
const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1)
const [selectedValue, setSelectedValue] = useState<Date | Date[]>()
@@ -60,7 +62,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
useEffect(() => {
if (Array.isArray(value) && value.length > 0) {
setSelectedValue(value.map(item => new Date(item)))
setCurrent(new Date(value[0]))
setCurrent(new Date(value[0] as Date))
}
if ((typeof value === 'string' || value instanceof Date) && value) {
setSelectedValue(new Date(value))
@@ -71,11 +73,13 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
useImperativeHandle(ref, () => ({
jumpTo: (year: number, month: number) => {
calendarRef.current?.jumpTo(year, month)
},
gotoMonth: (delta: number) => {
gotoMonth(delta)
}
}))
const handleDateChange = (newValue: any) => {
console.log('xxxxxxxxxxxxxxxxxxxxxx', newValue)
setSelectedValue(newValue)
onChange?.(newValue as any)
}
@@ -87,6 +91,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
}
const gotoMonth = (delta: number) => {
console.log('aaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbb', delta)
const base = current instanceof Date ? new Date(current) : new Date()
base.setMonth(base.getMonth() + delta)
const next = startOfMonth(base)
@@ -101,6 +106,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
}
const handleHeaderClick = () => {
console.log('handleHeaderClick', current)
onHeaderClick && onHeaderClick(current)
setvisible(true)
}
@@ -115,11 +121,11 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
}
}
const renderDay = (day: any) => {
const { date, month, year} = day;
const { date, month, year } = day;
const today = new Date()
if (date === today.getDate() && month === today.getMonth() + 1 && year === today.getFullYear()) {
return (
<View class="day-container">
<View className="day-container">
{date}
</View>
)
@@ -166,6 +172,15 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
</View>
}
<View className={`${styles['calendar-card']} ${isBorder ? styles['border'] : ''}`}>
{
type === 'range' && (
<View className={styles['date-range-container']}>
<Text className={`${styles['date-text-placeholder']} ${value?.[0] ? styles['date-text'] : ''}`}>{value?.[0] ? dayjs(value?.[0] as Date).format('YYYY-MM-DD') : '起始时间'}</Text>
<Text></Text>
<Text className={`${styles['date-text-placeholder']} ${value?.[1] ? styles['date-text'] : ''}`}>{value?.[1] ? dayjs(value?.[1] as Date).format('YYYY-MM-DD') : '结束时间'}</Text>
</View>
)
}
{/* 自定义头部显示周一到周日 */}
<View className={styles['header']}>
<View className={styles['header-left']} onClick={handleHeaderClick}>
@@ -182,7 +197,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
</View>
</View>
<View className={styles['week-header']}>
{[ '周日', '周一', '周二', '周三', '周四', '周五', '周六'].map((day) => (
{['周日', '周一', '周二', '周三', '周四', '周五', '周六'].map((day) => (
<Text key={day} className={styles['week-day']}>
{day}
</Text>
@@ -199,12 +214,12 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
onPageChange={handlePageChange}
/>
</View>
{ visible && <PopupPicker
{visible && <PopupPicker
visible={visible}
setvisible={setvisible}
value={[current.getFullYear(), current.getMonth() + 1]}
type="month"
onChange={(value) => handleMonthChange(value)}/> }
onChange={(value) => handleMonthChange(value)} />}
</View>
)
})

View File

@@ -1,35 +1,63 @@
.calendar-card {
background: #fff;
border-radius: 16px;
&.border{
&.border {
border-radius: 12px;
border: 0.5px solid rgba(0, 0, 0, 0.12);
margin-bottom: 6px;
padding: 12px 12px 8px;
}
}
}
.header {
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 4px 11px 4px;
height: 24px;
}
.header-left {
}
.date-range-container {
height: 55px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
color: #000;
padding: 0 4px;
font-size: 17.68px;
}
.date-text-placeholder {
font-family: PingFang SC;
font-weight: 600;
font-style: Semibold;
font-size: 17.68px;
color: #3C3C4399;
}
.date-text {
color: #000;
}
.header-left {
display: flex;
align-items: center;
gap: 6px;
}
.header-text {
}
.header-text {
font-size: 17px;
font-weight: 600;
color: #000;
}
.header-actions {
}
.header-actions {
display: flex;
width: 60px;
.arrow-left-container {
display: flex;
align-items: center;
@@ -37,6 +65,7 @@
width: 50%;
flex: 1;
}
.arrow-right-container {
display: flex;
align-items: center;
@@ -44,134 +73,145 @@
width: 50%;
flex: 1;
}
}
.month-arrow{
}
.month-arrow {
width: 8px;
height: 24px;
}
.arrow {
}
.arrow {
width: 10px;
height: 24px;
position: relative;
}
.arrow.left {
}
.arrow.left {
left: 9px;
transform: rotate(-180deg);
}
}
.week-row {
.week-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 0 0 4px 0;
}
.week-item {
}
.week-item {
text-align: center;
color: rgba(60, 60, 67, 0.30);
font-size: 13px;
}
}
// 新增的周一到周日头部样式
.week-header {
// 新增的周一到周日头部样式
.week-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 8px 0;
}
.week-day {
}
.week-day {
text-align: center;
color: rgba(60, 60, 67, 0.30);
font-size: 14px;
font-weight: 500;
}
}
.grid {
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px 0;
padding: 4px 0 16px;
}
.cell {
}
.cell {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
position: relative;
}
.cell.empty {
}
.cell.empty {
opacity: 0;
}
.cell.disabled {
color: rgba(0,0,0,0.2);
}
.cell-text.selected {
}
.cell.disabled {
color: rgba(0, 0, 0, 0.2);
}
.cell-text.selected {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.9);
background: rgba(0, 0, 0, 0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
// 时间段选择样式
.cell-text.range-start {
// 时间段选择样式
.cell-text.range-start {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.9);
background: rgba(0, 0, 0, 0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
.cell-text.range-end {
.cell-text.range-end {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.9);
background: rgba(0, 0, 0, 0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
.cell-text.in-range {
.cell-text.in-range {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.1);
background: rgba(0, 0, 0, 0.1);
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
}
.footer {
.footer {
display: flex;
gap: 12px;
}
.btn {
}
.btn {
flex: 1;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.06);
background: rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
}
.btn.primary {
}
.btn.primary {
background: #000;
color: #fff;
}
}
.hm-placeholder {
.hm-placeholder {
height: 240px;
display: flex;
align-items: center;
justify-content: center;
}
}
// CalendarRange 组件样式
.calendar-range {
@@ -241,43 +281,56 @@
.nut-calendarcard-header {
display: none !important;
}
.nut-calendarcard-content{
.nut-calendarcard-days{
&:first-child{
.nut-calendarcard-content {
.nut-calendarcard-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
justify-items: center;
&:first-child {
display: none !important;
}
}
}
}
.nut-calendarcard-day{
margin-bottom:0px!important;
.nut-calendarcard-day {
margin-bottom: 0px !important;
height: 44px;
width: 44px!important;
&.active{
background-color: #000!important;
color: #fff!important;
width: 44px !important;
&.active {
background-color: #000 !important;
color: #fff !important;
height: 44px;
border-radius: 22px!important;
border-radius: 22px !important;
display: flex;
align-items: center;
justify-content: center;
width: 44px!important;
font-size: 24px!important;
.day-container{
background-color: transparent!important;
width: 44px !important;
font-size: 24px !important;
.day-container {
background-color: transparent !important;
}
}
&.weekend{
color: rgb(0,0,0)!important;
&.active{
color: #fff!important;
&.weekend {
color: rgb(0, 0, 0) !important;
&.active {
color: #fff !important;
}
}
}
.nut-calendarcard-day-inner{
.nut-calendarcard-day-inner {
font-size: 20px;
.day-container{
.day-container {
background-color: #f5f5f5;
border-radius: 22px;
width: 44px;
@@ -288,5 +341,22 @@
font-size: 24px;
}
}
}
.nut-calendarcard-day.start,
.nut-calendarcard-day.end {
background-color: #000;
border-radius: 50%;
color: #fff !important;
}
.nut-calendarcard-day-inner .day-container {
background-color: unset;
color: unset;
}
.nut-calendarcard-day.mid {
background-color: rgba(0, 0, 0, 0.12);
color: #000;
border-radius: 50%;
}
}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { View, Text, Button } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro";
import dayjs from 'dayjs'
import "./index.scss";
import { DialogCalendarCard } from "@/components/index";
@@ -56,8 +57,13 @@ const DownloadBill: React.FC = () => {
}
};
const [currentTimeValue, setCurrentTimeValue] = useState<Date | Date[]>(new Date());
const handleConfirm = (val) => {
const handleConfirm = (val: Date[]) => {
setCurrentTimeValue(val);
const [start, end] = val;
setDateRange({
start: dayjs(start).format("YYYY-MM-DD"),
end: dayjs(end).format("YYYY-MM-DD"),
});
};
return (
<View className="download_bill_page">