日期范围选择组件

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 dayjs from 'dayjs'
import styles from './index.module.scss' import styles from './index.module.scss'
export interface DialogCalendarCardProps { export interface DialogCalendarCardProps {
value?: Date value?: Date | Date[]
searchType?: 'single' | 'range' | 'multiple' searchType?: 'single' | 'range' | 'multiple'
onChange?: (date: Date | Date[]) => void onChange?: (date: Date | Date[]) => void
visible: boolean visible: boolean
@@ -22,7 +22,7 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
value, value,
onChange, onChange,
}) => { }) => {
const [selected, setSelected] = useState<Date>(value || new Date()) const [selected, setSelected] = useState<Date | Date[]>(value || new Date())
const calendarRef = useRef<CalendarUIRef>(null); const calendarRef = useRef<CalendarUIRef>(null);
const [type, setType] = useState<'year' | 'month' | 'time'>('year'); const [type, setType] = useState<'year' | 'month' | 'time'>('year');
const [selectedHour, setSelectedHour] = useState(8) 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 [pendingJump, setPendingJump] = useState<{ year: number; month: number } | null>(null)
const handleConfirm = () => { const handleConfirm = () => {
if (type === 'year') { if (type === 'year') {
if (searchType === 'range') {
if (onChange) onChange(selected);
onClose();
return;
}
// 年份选择完成后,进入月份选择 // 年份选择完成后,进入月份选择
setType('time') setType('time')
} else if (type === 'month') { } else if (type === 'month') {
@@ -40,7 +45,13 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
if (value) { if (value) {
const year = value[0] as number const year = value[0] as number
const month = value[1] 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)) setSelected(new Date(year, month - 1, 1))
}
setPendingJump({ year, month }) setPendingJump({ year, month })
} }
setType('year') setType('year')
@@ -54,18 +65,31 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
setSelectedMinute(minute) setSelectedMinute(minute)
const hours = hour.toString().padStart(2, '0') const hours = hour.toString().padStart(2, '0')
const minutes = minute.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) if (onChange) onChange(finalDate)
} }
onClose() 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[]) => { const handleChange = (d: Date | Date[]) => {
console.log('handleChange', d)
if (searchType === 'range') { if (searchType === 'range') {
if (Array.isArray(d)) { if (Array.isArray(d)) {
if (d.length === 2) { if (d.length === 2) {
onChange?.(d as Date[]) setSelected(d as Date[])
return return
} }
} }
@@ -77,6 +101,7 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
} }
} }
const onHeaderClick = (date: Date) => { const onHeaderClick = (date: Date) => {
console.log('onHeaderClick', date)
setSelected(date) setSelected(date)
setType('month') setType('month')
} }
@@ -101,8 +126,8 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
useEffect(() => { useEffect(() => {
if (visible && value) { if (visible && value) {
setSelected(value || new Date()) setSelected(value || new Date())
setSelectedHour(value ? dayjs(value).hour() : 8) setSelectedHour(value ? dayjs(value as Date).hour() : 8)
setSelectedMinute(value ? dayjs(value).minute() : 0) setSelectedMinute(value ? dayjs(value as Date).minute() : 0)
} }
}, [value, visible]) }, [value, visible])
@@ -146,7 +171,7 @@ const DialogCalendarCard: React.FC<DialogCalendarCardProps> = ({
ref={pickerRef} ref={pickerRef}
onChange={handleDateTimePickerChange} onChange={handleDateTimePickerChange}
type="month" 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 styles from './index.module.scss'
import { getMonth, getWeekend, getWeekendOfCurrentWeek } from '@/utils/timeUtils' import { getMonth, getWeekend, getWeekendOfCurrentWeek } from '@/utils/timeUtils'
import { PopupPicker } from '@/components/Picker/index' import { PopupPicker } from '@/components/Picker/index'
import dayjs from 'dayjs'
interface NutUICalendarProps { interface NutUICalendarProps {
type?: 'single' | 'range' | 'multiple' type?: 'single' | 'range' | 'multiple'
value?: string | Date | String[] | Date[] value?: string | Date | String[] | Date[]
@@ -16,7 +17,8 @@ interface NutUICalendarProps {
} }
export interface CalendarUIRef { 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>(({ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
@@ -60,7 +62,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
useEffect(() => { useEffect(() => {
if (Array.isArray(value) && value.length > 0) { if (Array.isArray(value) && value.length > 0) {
setSelectedValue(value.map(item => new Date(item))) 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) { if ((typeof value === 'string' || value instanceof Date) && value) {
setSelectedValue(new Date(value)) setSelectedValue(new Date(value))
@@ -71,11 +73,13 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
jumpTo: (year: number, month: number) => { jumpTo: (year: number, month: number) => {
calendarRef.current?.jumpTo(year, month) calendarRef.current?.jumpTo(year, month)
},
gotoMonth: (delta: number) => {
gotoMonth(delta)
} }
})) }))
const handleDateChange = (newValue: any) => { const handleDateChange = (newValue: any) => {
console.log('xxxxxxxxxxxxxxxxxxxxxx', newValue)
setSelectedValue(newValue) setSelectedValue(newValue)
onChange?.(newValue as any) onChange?.(newValue as any)
} }
@@ -87,6 +91,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
} }
const gotoMonth = (delta: number) => { const gotoMonth = (delta: number) => {
console.log('aaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbb', delta)
const base = current instanceof Date ? new Date(current) : new Date() const base = current instanceof Date ? new Date(current) : new Date()
base.setMonth(base.getMonth() + delta) base.setMonth(base.getMonth() + delta)
const next = startOfMonth(base) const next = startOfMonth(base)
@@ -101,6 +106,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
} }
const handleHeaderClick = () => { const handleHeaderClick = () => {
console.log('handleHeaderClick', current)
onHeaderClick && onHeaderClick(current) onHeaderClick && onHeaderClick(current)
setvisible(true) setvisible(true)
} }
@@ -119,7 +125,7 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
const today = new Date() const today = new Date()
if (date === today.getDate() && month === today.getMonth() + 1 && year === today.getFullYear()) { if (date === today.getDate() && month === today.getMonth() + 1 && year === today.getFullYear()) {
return ( return (
<View class="day-container"> <View className="day-container">
{date} {date}
</View> </View>
) )
@@ -166,6 +172,15 @@ const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
</View> </View>
} }
<View className={`${styles['calendar-card']} ${isBorder ? styles['border'] : ''}`}> <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']}>
<View className={styles['header-left']} onClick={handleHeaderClick}> <View className={styles['header-left']} onClick={handleHeaderClick}>

View File

@@ -1,6 +1,7 @@
.calendar-card { .calendar-card {
background: #fff; background: #fff;
border-radius: 16px; border-radius: 16px;
&.border { &.border {
border-radius: 12px; border-radius: 12px;
border: 0.5px solid rgba(0, 0, 0, 0.12); border: 0.5px solid rgba(0, 0, 0, 0.12);
@@ -17,19 +18,46 @@
padding: 9px 4px 11px 4px; padding: 9px 4px 11px 4px;
height: 24px; height: 24px;
} }
.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 { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
} }
.header-text { .header-text {
font-size: 17px; font-size: 17px;
font-weight: 600; font-weight: 600;
color: #000; color: #000;
} }
.header-actions { .header-actions {
display: flex; display: flex;
width: 60px; width: 60px;
.arrow-left-container { .arrow-left-container {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -37,6 +65,7 @@
width: 50%; width: 50%;
flex: 1; flex: 1;
} }
.arrow-right-container { .arrow-right-container {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -45,15 +74,18 @@
flex: 1; flex: 1;
} }
} }
.month-arrow { .month-arrow {
width: 8px; width: 8px;
height: 24px; height: 24px;
} }
.arrow { .arrow {
width: 10px; width: 10px;
height: 24px; height: 24px;
position: relative; position: relative;
} }
.arrow.left { .arrow.left {
left: 9px; left: 9px;
transform: rotate(-180deg); transform: rotate(-180deg);
@@ -64,6 +96,7 @@
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
padding: 0 0 4px 0; padding: 0 0 4px 0;
} }
.week-item { .week-item {
text-align: center; text-align: center;
color: rgba(60, 60, 67, 0.30); color: rgba(60, 60, 67, 0.30);
@@ -76,6 +109,7 @@
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(7, 1fr);
padding: 8px 0; padding: 8px 0;
} }
.week-day { .week-day {
text-align: center; text-align: center;
color: rgba(60, 60, 67, 0.30); color: rgba(60, 60, 67, 0.30);
@@ -89,6 +123,7 @@
gap: 8px 0; gap: 8px 0;
padding: 4px 0 16px; padding: 4px 0 16px;
} }
.cell { .cell {
height: 44px; height: 44px;
display: flex; display: flex;
@@ -97,12 +132,15 @@
font-size: 20px; font-size: 20px;
position: relative; position: relative;
} }
.cell.empty { .cell.empty {
opacity: 0; opacity: 0;
} }
.cell.disabled { .cell.disabled {
color: rgba(0, 0, 0, 0.2); color: rgba(0, 0, 0, 0.2);
} }
.cell-text.selected { .cell-text.selected {
width: 44px; width: 44px;
height: 44px; height: 44px;
@@ -152,6 +190,7 @@
display: flex; display: flex;
gap: 12px; gap: 12px;
} }
.btn { .btn {
flex: 1; flex: 1;
height: 44px; height: 44px;
@@ -161,6 +200,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.btn.primary { .btn.primary {
background: #000; background: #000;
color: #fff; color: #fff;
@@ -241,8 +281,14 @@
.nut-calendarcard-header { .nut-calendarcard-header {
display: none !important; display: none !important;
} }
.nut-calendarcard-content { .nut-calendarcard-content {
.nut-calendarcard-days { .nut-calendarcard-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
justify-items: center;
&:first-child { &:first-child {
display: none !important; display: none !important;
} }
@@ -250,10 +296,12 @@
} }
} }
.nut-calendarcard-day { .nut-calendarcard-day {
margin-bottom: 0px !important; margin-bottom: 0px !important;
height: 44px; height: 44px;
width: 44px !important; width: 44px !important;
&.active { &.active {
background-color: #000 !important; background-color: #000 !important;
color: #fff !important; color: #fff !important;
@@ -264,19 +312,24 @@
justify-content: center; justify-content: center;
width: 44px !important; width: 44px !important;
font-size: 24px !important; font-size: 24px !important;
.day-container { .day-container {
background-color: transparent !important; background-color: transparent !important;
} }
} }
&.weekend { &.weekend {
color: rgb(0, 0, 0) !important; color: rgb(0, 0, 0) !important;
&.active { &.active {
color: #fff !important; color: #fff !important;
} }
} }
} }
.nut-calendarcard-day-inner { .nut-calendarcard-day-inner {
font-size: 20px; font-size: 20px;
.day-container { .day-container {
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 22px; border-radius: 22px;
@@ -288,5 +341,22 @@
font-size: 24px; 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 React, { useState, useEffect } from "react";
import { View, Text, Button } from "@tarojs/components"; import { View, Text, Button } from "@tarojs/components";
import Taro, { useDidShow } from "@tarojs/taro"; import Taro, { useDidShow } from "@tarojs/taro";
import dayjs from 'dayjs'
import "./index.scss"; import "./index.scss";
import { DialogCalendarCard } from "@/components/index"; import { DialogCalendarCard } from "@/components/index";
@@ -56,8 +57,13 @@ const DownloadBill: React.FC = () => {
} }
}; };
const [currentTimeValue, setCurrentTimeValue] = useState<Date | Date[]>(new Date()); const [currentTimeValue, setCurrentTimeValue] = useState<Date | Date[]>(new Date());
const handleConfirm = (val) => { const handleConfirm = (val: Date[]) => {
setCurrentTimeValue(val); setCurrentTimeValue(val);
const [start, end] = val;
setDateRange({
start: dayjs(start).format("YYYY-MM-DD"),
end: dayjs(end).format("YYYY-MM-DD"),
});
}; };
return ( return (
<View className="download_bill_page"> <View className="download_bill_page">