返回 AiToEarn
1 /**
2 * PublishDatePicker Component
3 * 发布日期选择器组件 - 支持立即发布和定时发布两种模式
4 * - 点击左侧按钮打开日期选择器
5 * - 底部 Now 按钮:选择立即发布
6 * - 底部 Confirm 按钮:确认定时发布
7 */
8
9 import type { Dayjs } from 'dayjs'
10 import type { ForwardedRef } from 'react'
11 import dayjs from 'dayjs'
12 import { Check, ChevronDown, Clock, Loader2, Send } from 'lucide-react'
13 import { forwardRef, memo, useEffect, useMemo, useState } from 'react'
14 import { useShallow } from 'zustand/react/shallow'
15 import { useTransClient } from '@/app/i18n/client'
16 import { usePublishDialog } from '@/components/PublishDialog/usePublishDialog'
17 import { Button } from '@/components/ui/button'
18 import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
19 import { cn } from '@/utils/className'
20 import { DateTimePicker } from './DateTimePicker'
21
22 export interface IPublishDatePickerRef {}
23
24 export interface IPublishDatePickerProps {
25 loading: boolean
26 onClick: (pubTime?: string) => unknown | Promise<unknown>
27 // 是否为移动端
28 isMobile?: boolean
29 value?: string
30 onValueChange?: (pubTime?: string) => void
31 inline?: boolean
32 showNowButton?: boolean
33 submitText?: string
34 minLeadMinutes?: number
35 }
36
37 const PublishDatePicker = memo(
38 forwardRef(
39 (
40 {
41 loading,
42 onClick,
43 isMobile,
44 value,
45 onValueChange,
46 inline = false,
47 showNowButton = true,
48 submitText,
49 minLeadMinutes = 20,
50 }: IPublishDatePickerProps,
51 ref: ForwardedRef<IPublishDatePickerRef>,
52 ) => {
53 const { t } = useTransClient('publish')
54 const [menuOpen, setMenuOpen] = useState(false)
55 const [tempDate, setTempDate] = useState<Dayjs | null>(null)
56 const { pubTime, setPubTime } = usePublishDialog(
57 useShallow(state => ({
58 pubTime: state.pubTime,
59 setPubTime: state.setPubTime,
60 })),
61 )
62 const currentPubTime = onValueChange ? value : pubTime
63 const updatePubTime = onValueChange ?? setPubTime
64
65 const handleCalendarChange = (date: Dayjs) => {
66 setTempDate(date)
67 }
68
69 // 点击 Now 按钮 - 选择立即发布模式
70 const handleNowClick = (e: React.MouseEvent) => {
71 e.stopPropagation()
72 updatePubTime(undefined)
73 setMenuOpen(false)
74 }
75
76 // 点击 Confirm 按钮 - 确认定时发布时间
77 const handleScheduleConfirm = async (e: React.MouseEvent) => {
78 e.stopPropagation()
79 const nextPubTime = tempDate ? tempDate.format() : undefined
80 updatePubTime(nextPubTime)
81 setMenuOpen(false)
82 if (inline) {
83 await onClick(nextPubTime)
84 }
85 }
86
87 // 禁用今天之前的日期
88 const disabledDate = (current: dayjs.Dayjs) => {
89 return current && current < dayjs().startOf('day')
90 }
91
92 // 禁用当前时间之前和当前时间20分钟内的时间(只精确到分钟,不考虑秒)
93 const disabledDateTime = (current: dayjs.Dayjs | null) => {
94 if (!current)
95 return {}
96
97 const now = dayjs()
98 const minTime = now.add(minLeadMinutes, 'minute')
99
100 // 只对今天限制
101 if (current.isSame(now, 'day')) {
102 const minHour = minTime.hour()
103 const minMinute = minTime.minute()
104
105 // 禁用小时
106 const disabledHours = () => {
107 const hours: number[] = []
108 for (let i = 0; i < 24; i++) {
109 if (i < minHour)
110 hours.push(i)
111 }
112 return hours
113 }
114
115 // 禁用分钟
116 const disabledMinutes = (selectedHour: number) => {
117 const minutes: number[] = []
118 if (selectedHour === minHour) {
119 for (let i = 0; i < minMinute; i++) {
120 minutes.push(i)
121 }
122 }
123 else if (selectedHour < minHour) {
124 // 小时都禁用了,分钟全部禁用
125 for (let i = 0; i < 60; i++) {
126 minutes.push(i)
127 }
128 }
129 return minutes
130 }
131
132 return {
133 disabledHours,
134 disabledMinutes,
135 }
136 }
137 // 非今天不限制
138 return {}
139 }
140
141 const pubTimeValue = useMemo(() => {
142 if (currentPubTime) {
143 return dayjs(currentPubTime)
144 }
145 return null
146 }, [currentPubTime])
147
148 useEffect(() => {
149 if (inline) {
150 setTempDate(pubTimeValue || dayjs().add(30, 'minute'))
151 }
152 }, [inline, pubTimeValue])
153
154 // 当打开弹窗时,初始化临时日期
155 const handleOpenChange = (open: boolean) => {
156 if (open) {
157 // 如果已有定时时间,使用它;否则使用当前时间+30分钟作为默认
158 setTempDate(pubTimeValue || dayjs().add(30, 'minute'))
159 }
160 setMenuOpen(open)
161 }
162
163 // 日期选择器内容
164 const datePickerContent = (
165 <div className={cn(isMobile ? 'w-full' : 'w-[400px]')}>
166 <DateTimePicker
167 value={tempDate}
168 onChange={handleCalendarChange}
169 disabledDate={disabledDate}
170 disabledTime={disabledDateTime}
171 isMobile={isMobile}
172 />
173
174 <div className="flex items-center justify-between py-2.5 px-4 border-t border-border">
175 {showNowButton ? (
176 <Button
177 variant="ghost"
178 size="sm"
179 onClick={handleNowClick}
180 className="gap-2 cursor-pointer"
181 data-testid="publish-now-button"
182 >
183 <Send className="w-4 h-4" />
184 {t('buttons.publishNow')}
185 </Button>
186 ) : <span />}
187
188 <Button
189 variant="ghost"
190 size="sm"
191 onClick={handleScheduleConfirm}
192 className="gap-2 cursor-pointer"
193 disabled={loading || !tempDate}
194 data-testid="publish-schedule-confirm-button"
195 >
196 {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
197 {submitText ?? t('buttons.confirm')}
198 </Button>
199 </div>
200 </div>
201 )
202
203 if (inline) {
204 return (
205 <div className={cn('w-full', isMobile && 'w-full')} data-testid="publish-date-picker">
206 {datePickerContent}
207 </div>
208 )
209 }
210
211 return (
212 <div className={cn('flex gap-0', isMobile && 'w-full')} data-testid="publish-date-picker">
213 <Popover open={menuOpen} onOpenChange={handleOpenChange}>
214 <PopoverTrigger asChild>
215 <Button
216 variant="outline"
217 className={cn(
218 'flex items-center gap-2.5 rounded-r-none border-r-0 h-10 px-4 cursor-pointer',
219 isMobile && 'flex-1',
220 )}
221 onClick={() => {
222 setMenuOpen(true)
223 }}
224 data-testid="publish-schedule-trigger"
225 >
226 {pubTimeValue ? <Clock className="w-4 h-4" /> : <Send className="w-4 h-4" />}
227 <span className="flex-1 text-left">
228 {pubTimeValue ? pubTimeValue.format('YYYY-MM-DD HH:mm') : t('buttons.publishNow')}
229 </span>
230 <ChevronDown className="w-4 h-4" />
231 </Button>
232 </PopoverTrigger>
233 <PopoverContent
234 className={cn('w-auto p-0', isMobile && 'w-[calc(100vw-32px)]')}
235 align="start"
236 allowInnerScroll
237 >
238 {datePickerContent}
239 </PopoverContent>
240 </Popover>
241 <Button
242 className={cn('rounded-l-none h-10 px-6 cursor-pointer', isMobile && 'shrink-0')}
243 disabled={loading}
244 onClick={() => onClick(currentPubTime)}
245 data-testid="publish-submit-btn"
246 >
247 {loading && <Loader2 className="-ml-1 mr-2 h-4 w-4 animate-spin" />}
248 {t('publish')}
249 </Button>
250 </div>
251 )
252 },
253 ),
254 )
255
256 PublishDatePicker.displayName = 'PublishDatePicker'
257
258 export default PublishDatePicker
259
259 lines Plain Text