mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-03 07:25:53 +00:00
实现 #584
This commit is contained in:
@@ -75,6 +75,8 @@
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
@@ -172,6 +174,33 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.year-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
|
||||
.year-btn {
|
||||
padding: 10px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,4 +401,4 @@
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { X, ChevronLeft, ChevronRight, Calendar as CalendarIcon, Loader2 } from 'lucide-react'
|
||||
import './JumpToDateDialog.scss'
|
||||
|
||||
@@ -21,10 +21,15 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
|
||||
messageDates,
|
||||
loadingDates = false
|
||||
}) => {
|
||||
type CalendarViewMode = 'day' | 'month' | 'year'
|
||||
const getYearPageStart = (year: number): number => Math.floor(year / 12) * 12
|
||||
const isValidDate = (d: any) => d instanceof Date && !isNaN(d.getTime())
|
||||
const [calendarDate, setCalendarDate] = useState(isValidDate(currentDate) ? new Date(currentDate) : new Date())
|
||||
const [selectedDate, setSelectedDate] = useState(new Date(currentDate))
|
||||
const [showYearMonthPicker, setShowYearMonthPicker] = useState(false)
|
||||
const [viewMode, setViewMode] = useState<CalendarViewMode>('day')
|
||||
const [yearPageStart, setYearPageStart] = useState<number>(
|
||||
getYearPageStart((isValidDate(currentDate) ? new Date(currentDate) : new Date()).getFullYear())
|
||||
)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
@@ -116,6 +121,57 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
|
||||
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const days = generateCalendar()
|
||||
const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
|
||||
|
||||
const updateCalendarDate = (nextDate: Date) => {
|
||||
setCalendarDate(nextDate)
|
||||
}
|
||||
|
||||
const openMonthView = () => setViewMode('month')
|
||||
const openYearView = () => {
|
||||
setYearPageStart(getYearPageStart(calendarDate.getFullYear()))
|
||||
setViewMode('year')
|
||||
}
|
||||
|
||||
const handleTitleClick = () => {
|
||||
if (viewMode === 'day') {
|
||||
openMonthView()
|
||||
return
|
||||
}
|
||||
if (viewMode === 'month') {
|
||||
openYearView()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (viewMode === 'day') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))
|
||||
return
|
||||
}
|
||||
if (viewMode === 'month') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear() - 1, calendarDate.getMonth(), 1))
|
||||
return
|
||||
}
|
||||
setYearPageStart((prev) => prev - 12)
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (viewMode === 'day') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))
|
||||
return
|
||||
}
|
||||
if (viewMode === 'month') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear() + 1, calendarDate.getMonth(), 1))
|
||||
return
|
||||
}
|
||||
setYearPageStart((prev) => prev + 12)
|
||||
}
|
||||
|
||||
const navTitle = viewMode === 'day'
|
||||
? `${calendarDate.getFullYear()}年${calendarDate.getMonth() + 1}月`
|
||||
: viewMode === 'month'
|
||||
? `${calendarDate.getFullYear()}年`
|
||||
: `${yearPageStart}年 - ${yearPageStart + 11}年`
|
||||
|
||||
return (
|
||||
<div className="jump-date-overlay" onClick={onClose}>
|
||||
@@ -134,45 +190,57 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
|
||||
<div className="calendar-nav">
|
||||
<button
|
||||
className="nav-btn"
|
||||
onClick={() => setCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))}
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
</button>
|
||||
<span className="current-month clickable" onClick={() => setShowYearMonthPicker(!showYearMonthPicker)}>
|
||||
{calendarDate.getFullYear()}年{calendarDate.getMonth() + 1}月
|
||||
</span>
|
||||
<button
|
||||
className={`current-month ${viewMode === 'year' ? '' : 'clickable'}`.trim()}
|
||||
onClick={handleTitleClick}
|
||||
type="button"
|
||||
>
|
||||
{navTitle}
|
||||
</button>
|
||||
<button
|
||||
className="nav-btn"
|
||||
onClick={() => setCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))}
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showYearMonthPicker ? (
|
||||
{viewMode === 'month' ? (
|
||||
<div className="year-month-picker">
|
||||
<div className="year-selector">
|
||||
<button className="nav-btn" onClick={() => setCalendarDate(new Date(calendarDate.getFullYear() - 1, calendarDate.getMonth(), 1))}>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<span className="year-label">{calendarDate.getFullYear()}年</span>
|
||||
<button className="nav-btn" onClick={() => setCalendarDate(new Date(calendarDate.getFullYear() + 1, calendarDate.getMonth(), 1))}>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="month-grid">
|
||||
{['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'].map((name, i) => (
|
||||
{monthNames.map((name, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className={`month-btn ${i === calendarDate.getMonth() ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setCalendarDate(new Date(calendarDate.getFullYear(), i, 1))
|
||||
setShowYearMonthPicker(false)
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear(), i, 1))
|
||||
setViewMode('day')
|
||||
}}
|
||||
>{name}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : viewMode === 'year' ? (
|
||||
<div className="year-month-picker">
|
||||
<div className="year-grid">
|
||||
{Array.from({ length: 12 }, (_, i) => yearPageStart + i).map((year) => (
|
||||
<button
|
||||
key={year}
|
||||
className={`year-btn ${year === calendarDate.getFullYear() ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
updateCalendarDate(new Date(year, calendarDate.getMonth(), 1))
|
||||
setViewMode('month')
|
||||
}}
|
||||
>
|
||||
{year}年
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`calendar-grid ${loadingDates ? 'loading' : ''}`}>
|
||||
{loadingDates && (
|
||||
@@ -208,18 +276,21 @@ const JumpToDateDialog: React.FC<JumpToDateDialogProps> = ({
|
||||
const d = new Date()
|
||||
setSelectedDate(d)
|
||||
setCalendarDate(new Date(d))
|
||||
setViewMode('day')
|
||||
}}>今天</button>
|
||||
<button onClick={() => {
|
||||
const d = new Date()
|
||||
d.setDate(d.getDate() - 7)
|
||||
setSelectedDate(d)
|
||||
setCalendarDate(new Date(d))
|
||||
setViewMode('day')
|
||||
}}>一周前</button>
|
||||
<button onClick={() => {
|
||||
const d = new Date()
|
||||
d.setMonth(d.getMonth() - 1)
|
||||
setSelectedDate(d)
|
||||
setCalendarDate(new Date(d))
|
||||
setViewMode('day')
|
||||
}}>一月前</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,6 +28,20 @@
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.jump-date-popover .current-month.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.jump-date-popover .current-month.clickable:hover {
|
||||
color: var(--primary);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.jump-date-popover .nav-btn {
|
||||
@@ -83,6 +97,37 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.jump-date-popover .month-grid,
|
||||
.jump-date-popover .year-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
min-height: 256px;
|
||||
}
|
||||
|
||||
.jump-date-popover .month-cell,
|
||||
.jump-date-popover .year-cell {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
.jump-date-popover .month-cell:hover,
|
||||
.jump-date-popover .year-cell:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.jump-date-popover .month-cell.active,
|
||||
.jump-date-popover .year-cell.active {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.jump-date-popover .day-cell {
|
||||
position: relative;
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -31,14 +31,20 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
loadingDates = false,
|
||||
loadingDateCounts = false
|
||||
}) => {
|
||||
type CalendarViewMode = 'day' | 'month' | 'year'
|
||||
const getYearPageStart = (year: number): number => Math.floor(year / 12) * 12
|
||||
const [calendarDate, setCalendarDate] = useState<Date>(new Date(currentDate))
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date(currentDate))
|
||||
const [viewMode, setViewMode] = useState<CalendarViewMode>('day')
|
||||
const [yearPageStart, setYearPageStart] = useState<number>(getYearPageStart(new Date(currentDate).getFullYear()))
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
const normalized = new Date(currentDate)
|
||||
setCalendarDate(normalized)
|
||||
setSelectedDate(normalized)
|
||||
setViewMode('day')
|
||||
setYearPageStart(getYearPageStart(normalized.getFullYear()))
|
||||
}, [isOpen, currentDate])
|
||||
|
||||
if (!isOpen) return null
|
||||
@@ -114,25 +120,78 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
const days = generateCalendar()
|
||||
const mergedClassName = ['jump-date-popover', className || ''].join(' ').trim()
|
||||
|
||||
const updateCalendarDate = (nextDate: Date) => {
|
||||
setCalendarDate(nextDate)
|
||||
onMonthChange?.(nextDate)
|
||||
}
|
||||
|
||||
const openMonthView = () => setViewMode('month')
|
||||
const openYearView = () => {
|
||||
setYearPageStart(getYearPageStart(calendarDate.getFullYear()))
|
||||
setViewMode('year')
|
||||
}
|
||||
|
||||
const handleTitleClick = () => {
|
||||
if (viewMode === 'day') {
|
||||
openMonthView()
|
||||
return
|
||||
}
|
||||
if (viewMode === 'month') {
|
||||
openYearView()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (viewMode === 'day') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))
|
||||
return
|
||||
}
|
||||
if (viewMode === 'month') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear() - 1, calendarDate.getMonth(), 1))
|
||||
return
|
||||
}
|
||||
setYearPageStart((prev) => prev - 12)
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (viewMode === 'day') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))
|
||||
return
|
||||
}
|
||||
if (viewMode === 'month') {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear() + 1, calendarDate.getMonth(), 1))
|
||||
return
|
||||
}
|
||||
setYearPageStart((prev) => prev + 12)
|
||||
}
|
||||
|
||||
const navTitle = viewMode === 'day'
|
||||
? `${calendarDate.getFullYear()}年${calendarDate.getMonth() + 1}月`
|
||||
: viewMode === 'month'
|
||||
? `${calendarDate.getFullYear()}年`
|
||||
: `${yearPageStart}年 - ${yearPageStart + 11}年`
|
||||
|
||||
return (
|
||||
<div className={mergedClassName} style={style} role="dialog" aria-label="跳转日期">
|
||||
<div className="calendar-nav">
|
||||
<button
|
||||
className="nav-btn"
|
||||
onClick={() => updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1))}
|
||||
onClick={handlePrev}
|
||||
aria-label="上一月"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<span className="current-month">{calendarDate.getFullYear()}年{calendarDate.getMonth() + 1}月</span>
|
||||
<button
|
||||
className={`current-month ${viewMode === 'year' ? '' : 'clickable'}`.trim()}
|
||||
onClick={handleTitleClick}
|
||||
type="button"
|
||||
>
|
||||
{navTitle}
|
||||
</button>
|
||||
<button
|
||||
className="nav-btn"
|
||||
onClick={() => updateCalendarDate(new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1))}
|
||||
onClick={handleNext}
|
||||
aria-label="下一月"
|
||||
>
|
||||
<ChevronRight size={16} />
|
||||
@@ -154,36 +213,74 @@ const JumpToDatePopover: React.FC<JumpToDatePopoverProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="calendar-grid">
|
||||
<div className="weekdays">
|
||||
{weekdays.map(day => (
|
||||
<div key={day} className="weekday">{day}</div>
|
||||
{viewMode === 'day' && (
|
||||
<div className="calendar-grid">
|
||||
<div className="weekdays">
|
||||
{weekdays.map(day => (
|
||||
<div key={day} className="weekday">{day}</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="days">
|
||||
{days.map((day, index) => {
|
||||
if (day === null) return <div key={index} className="day-cell empty" />
|
||||
const dateKey = toDateKey(day)
|
||||
const hasMessageOnDay = hasMessage(day)
|
||||
const count = Number(messageDateCounts?.[dateKey] || 0)
|
||||
const showCount = count > 0
|
||||
const showCountLoading = hasMessageOnDay && loadingDateCounts && !showCount
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={getDayClassName(day)}
|
||||
onClick={() => handleDateClick(day)}
|
||||
disabled={hasLoadedMessageDates && !hasMessageOnDay}
|
||||
type="button"
|
||||
>
|
||||
<span className="day-number">{day}</span>
|
||||
{showCount && <span className="day-count">{count}</span>}
|
||||
{showCountLoading && <Loader2 size={11} className="day-count-loading spin" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewMode === 'month' && (
|
||||
<div className="month-grid">
|
||||
{['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'].map((name, monthIndex) => (
|
||||
<button
|
||||
key={name}
|
||||
className={`month-cell ${monthIndex === calendarDate.getMonth() ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
updateCalendarDate(new Date(calendarDate.getFullYear(), monthIndex, 1))
|
||||
setViewMode('day')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="days">
|
||||
{days.map((day, index) => {
|
||||
if (day === null) return <div key={index} className="day-cell empty" />
|
||||
const dateKey = toDateKey(day)
|
||||
const hasMessageOnDay = hasMessage(day)
|
||||
const count = Number(messageDateCounts?.[dateKey] || 0)
|
||||
const showCount = count > 0
|
||||
const showCountLoading = hasMessageOnDay && loadingDateCounts && !showCount
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
className={getDayClassName(day)}
|
||||
onClick={() => handleDateClick(day)}
|
||||
disabled={hasLoadedMessageDates && !hasMessageOnDay}
|
||||
type="button"
|
||||
>
|
||||
<span className="day-number">{day}</span>
|
||||
{showCount && <span className="day-count">{count}</span>}
|
||||
{showCountLoading && <Loader2 size={11} className="day-count-loading spin" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
)}
|
||||
|
||||
{viewMode === 'year' && (
|
||||
<div className="year-grid">
|
||||
{Array.from({ length: 12 }, (_, i) => yearPageStart + i).map((year) => (
|
||||
<button
|
||||
key={year}
|
||||
className={`year-cell ${year === calendarDate.getFullYear() ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
updateCalendarDate(new Date(year, calendarDate.getMonth(), 1))
|
||||
setViewMode('month')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{year}年
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user