修改日历组件

This commit is contained in:
筱野
2025-09-07 20:26:32 +08:00
parent f503bf53ac
commit 549f704c53
22 changed files with 1057 additions and 563 deletions

View File

@@ -0,0 +1,211 @@
import React, { useState, useEffect, useRef, useImperativeHandle } from 'react'
import { CalendarCard } from '@nutui/nutui-react-taro'
import { View, Text, Image } from '@tarojs/components'
import images from '@/config/images'
import styles from './index.module.scss'
import { getMonth, getWeekend, getWeekendOfCurrentWeek } from '@/utils/timeUtils'
import { PopupPicker } from '@/components/Picker/index'
interface NutUICalendarProps {
type?: 'single' | 'range' | 'multiple'
value?: string | Date | String[] | Date[]
defaultValue?: string | string[]
onChange?: (value: Date | Date[]) => void,
isBorder?: boolean
showQuickActions?: boolean,
onHeaderClick?: (date: Date) => void
}
export interface CalendarUIRef {
jumpTo: (year: number, month: number) => void
}
const NutUICalendar = React.forwardRef<CalendarUIRef, NutUICalendarProps>(({
type = 'single',
value,
onChange,
isBorder = false,
showQuickActions = true,
onHeaderClick
}, ref) => {
// 根据类型初始化选中值
// 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
// }
const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1)
const [selectedValue, setSelectedValue] = useState<Date | Date[]>()
const [current, setCurrent] = useState<Date>(startOfMonth(new Date()))
const calendarRef = useRef<any>(null)
const [visible, setvisible] = useState(false)
console.log('current', current)
// 当外部 value 变化时更新内部状态
useEffect(() => {
if (Array.isArray(value) && value.length > 0) {
setSelectedValue(value.map(item => new Date(item)))
setCurrent(new Date(value[0]))
}
if ((typeof value === 'string' || value instanceof Date) && value) {
setSelectedValue(new Date(value))
setCurrent(new Date(value))
}
}, [value])
useImperativeHandle(ref, () => ({
jumpTo: (year: number, month: number) => {
calendarRef.current?.jumpTo(year, month)
}
}))
const handleDateChange = (newValue: any) => {
setSelectedValue(newValue)
onChange?.(newValue as any)
}
const formatHeader = (date: Date) => `${getMonth(date)}`
const handlePageChange = (data: { year: number; month: number }) => {
// 月份切换时的处理逻辑,如果需要的话
console.log('月份切换:', data)
}
const gotoMonth = (delta: number) => {
const base = current instanceof Date ? new Date(current) : new Date()
base.setMonth(base.getMonth() + delta)
const next = startOfMonth(base)
setCurrent(next)
// 同步底部 CalendarCard 的月份
try {
calendarRef.current?.jump?.(delta)
} catch (e) {
console.warn('CalendarCardRef jump 调用失败', e)
}
handlePageChange({ year: next.getFullYear(), month: next.getMonth() + 1 })
}
const handleHeaderClick = () => {
onHeaderClick && onHeaderClick(current)
setvisible(true)
}
const syncMonthTo = (anchor: Date) => {
// 计算从 current 到目标 anchor 所在月份的偏移,调用 jump(delta)
const monthsDelta = (anchor.getFullYear() - current.getFullYear()) * 12 + (anchor.getMonth() - current.getMonth())
if (monthsDelta !== 0) {
gotoMonth(monthsDelta)
}
}
const renderDay = (day: any) => {
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">
{date}
</View>
)
}
return date
}
const selectWeekend = () => {
const [start, end] = getWeekend()
setSelectedValue([start, end])
syncMonthTo(start)
onChange?.([start, end])
}
const selectWeek = () => {
const dayList = getWeekendOfCurrentWeek(7)
setSelectedValue(dayList)
syncMonthTo(dayList[0])
onChange?.(dayList)
}
const selectMonth = () => {
const dayList = getWeekendOfCurrentWeek(30)
setSelectedValue(dayList)
syncMonthTo(dayList[0])
onChange?.(dayList)
}
const handleMonthChange = (value: any) => {
const [year, month] = value;
const newDate = new Date(year, month - 1, 1);
setCurrent(newDate);
calendarRef.current?.jumpTo(year, month)
}
return (
<View>
{/* 快速操作行 */}
{
showQuickActions &&
<View className={styles['quick-actions']}>
<View className={styles['quick-action']} onClick={selectWeekend}></View>
<View className={styles['quick-action']} onClick={selectWeek}></View>
<View className={styles['quick-action']} onClick={selectMonth}></View>
</View>
}
<View className={`${styles['calendar-card']} ${isBorder ? styles['border'] : ''}`}>
{/* 自定义头部显示周一到周日 */}
<View className={styles['header']}>
<View className={styles['header-left']} onClick={handleHeaderClick}>
<Text className={styles['header-text']}>{formatHeader(current as Date)}</Text>
<Image src={images.ICON_RIGHT_MAX} className={`${styles['month-arrow']}`} onClick={() => gotoMonth(1)} />
</View>
<View className={styles['header-actions']}>
<View className={styles['arrow-left-container']} onClick={() => gotoMonth(-1)}>
<Image src={images.ICON_RIGHT_MAX} className={`${styles['arrow']} ${styles['left']}`} />
</View>
<View className={styles['arrow-right-container']} onClick={() => gotoMonth(1)}>
<Image src={images.ICON_RIGHT_MAX} className={`${styles['arrow']}`} />
</View>
</View>
</View>
<View className={styles['week-header']}>
{[ '周日', '周一', '周二', '周三', '周四', '周五', '周六'].map((day) => (
<Text key={day} className={styles['week-day']}>
{day}
</Text>
))}
</View>
{/* NutUI CalendarCard 组件 */}
<CalendarCard
ref={calendarRef}
type={type}
value={selectedValue}
renderDay={renderDay}
onChange={handleDateChange}
onPageChange={handlePageChange}
/>
</View>
{ visible && <PopupPicker
visible={visible}
setvisible={setvisible}
value={[current.getFullYear(), current.getMonth() + 1]}
type="month"
onChange={(value) => handleMonthChange(value)}/> }
</View>
)
})
export default NutUICalendar

View File

@@ -0,0 +1,292 @@
.calendar-card {
background: #fff;
border-radius: 16px;
&.border{
border-radius: 12px;
border: 0.5px solid rgba(0, 0, 0, 0.12);
margin-bottom: 6px;
padding: 12px 12px 8px;
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 9px 4px 11px 4px;
height: 24px;
}
.header-left {
display: flex;
align-items: center;
gap: 6px;
}
.header-text {
font-size: 17px;
font-weight: 600;
color: #000;
}
.header-actions {
display: flex;
width: 60px;
.arrow-left-container {
display: flex;
align-items: center;
justify-content: flex-start;
width: 50%;
flex: 1;
}
.arrow-right-container {
display: flex;
align-items: center;
justify-content: flex-end;
width: 50%;
flex: 1;
}
}
.month-arrow{
width: 8px;
height: 24px;
}
.arrow {
width: 10px;
height: 24px;
position: relative;
}
.arrow.left {
left: 9px;
transform: rotate(-180deg);
}
.week-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 0 0 4px 0;
}
.week-item {
text-align: center;
color: rgba(60, 60, 67, 0.30);
font-size: 13px;
}
// 新增的周一到周日头部样式
.week-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
padding: 8px 0;
}
.week-day {
text-align: center;
color: rgba(60, 60, 67, 0.30);
font-size: 14px;
font-weight: 500;
}
.grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px 0;
padding: 4px 0 16px;
}
.cell {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
position: relative;
}
.cell.empty {
opacity: 0;
}
.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);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
// 时间段选择样式
.cell-text.range-start {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.cell-text.range-end {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.9);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.cell-text.in-range {
width: 44px;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.1);
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
.footer {
display: flex;
gap: 12px;
}
.btn {
flex: 1;
height: 44px;
border-radius: 22px;
background: rgba(0,0,0,0.06);
display: flex;
align-items: center;
justify-content: center;
}
.btn.primary {
background: #000;
color: #fff;
}
.hm-placeholder {
height: 240px;
display: flex;
align-items: center;
justify-content: center;
}
// CalendarRange 组件样式
.calendar-range {
background: #fff;
border-radius: 16px;
overflow: hidden;
}
.quick-select {
display: flex;
padding: 16px 12px 12px;
gap: 8px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.quick-btn {
flex: 1;
height: 36px;
border-radius: 18px;
background: rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.quick-btn.active {
background: rgba(0, 0, 0, 0.9);
}
.quick-btn-text {
font-size: 14px;
color: #000;
font-weight: 500;
}
.quick-btn.active .quick-btn-text {
color: #fff;
}
.quick-actions {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.quick-action {
border-radius: 999px;
border: 0.5px solid rgba(0, 0, 0, 0.12);
background: #FFF;
display: flex;
height: 28px;
padding: 4px 10px;
justify-content: center;
align-items: center;
font-size: 14px;
color: #000;
flex: 1;
}
// 隐藏 CalendarCard 默认头部
:global {
.nut-calendarcard {
.nut-calendarcard-header {
display: none !important;
}
.nut-calendarcard-content{
.nut-calendarcard-days{
&:first-child{
display: none !important;
}
}
}
}
.nut-calendarcard-day{
margin-bottom:0px!important;
height: 44px;
width: 44px!important;
&.active{
background-color: #000!important;
color: #fff!important;
height: 44px;
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;
}
}
&.weekend{
color: rgb(0,0,0)!important;
&.active{
color: #fff!important;
}
}
}
.nut-calendarcard-day-inner{
font-size: 20px;
.day-container{
background-color: #f5f5f5;
border-radius: 22px;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
}
}