schedule-modal.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. 'use client'
  2. import type { FC } from 'react'
  3. import { useState } from 'react'
  4. import { RiCalendarScheduleLine, RiTimeLine } from '@remixicon/react'
  5. import Modal from '@/app/components/base/modal'
  6. import Button from '@/app/components/base/button'
  7. import { ScheduleAction } from '@/models/document-schedule'
  8. import type { CreateScheduleRequest } from '@/models/document-schedule'
  9. import { useCreateSchedule } from '@/service/knowledge/use-document-schedule'
  10. import { useToastContext } from '@/app/components/base/toast'
  11. import { useQueryClient } from '@tanstack/react-query'
  12. import { useScheduleListKey } from '@/service/knowledge/use-document-schedule'
  13. type ScheduleModalProps = {
  14. isOpen: boolean
  15. onClose: () => void
  16. datasetId: string
  17. documentId: string
  18. documentName: string
  19. }
  20. const ScheduleModal: FC<ScheduleModalProps> = ({
  21. isOpen,
  22. onClose,
  23. datasetId,
  24. documentId,
  25. documentName,
  26. }) => {
  27. const { notify } = useToastContext()
  28. const queryClient = useQueryClient()
  29. const [action, setAction] = useState<ScheduleAction>(ScheduleAction.ENABLE)
  30. const [scheduledDate, setScheduledDate] = useState('')
  31. const [scheduledTime, setScheduledTime] = useState('')
  32. const [reason, setReason] = useState('')
  33. const [notes, setNotes] = useState('')
  34. const { mutateAsync: createSchedule, isPending } = useCreateSchedule()
  35. const handleSubmit = async () => {
  36. if (!scheduledDate || !scheduledTime) {
  37. notify({ type: 'error', message: '请选择日期和时间' })
  38. return
  39. }
  40. // Combine date and time to ISO format
  41. const scheduledAt = new Date(`${scheduledDate}T${scheduledTime}`).toISOString()
  42. // Validate future time
  43. if (new Date(scheduledAt) <= new Date()) {
  44. notify({ type: 'error', message: '定时时间必须在未来' })
  45. return
  46. }
  47. const body: CreateScheduleRequest = {
  48. action,
  49. scheduled_at: scheduledAt,
  50. metadata: {
  51. reason: reason || undefined,
  52. notes: notes || undefined,
  53. },
  54. }
  55. try {
  56. await createSchedule({
  57. datasetId,
  58. documentId,
  59. body,
  60. })
  61. notify({
  62. type: 'success',
  63. message: '定时任务创建成功',
  64. })
  65. // Invalidate schedules query
  66. queryClient.invalidateQueries({
  67. queryKey: useScheduleListKey(datasetId, documentId),
  68. })
  69. handleClose()
  70. }
  71. catch (error: any) {
  72. notify({
  73. type: 'error',
  74. message: error.message || '定时任务创建失败',
  75. })
  76. }
  77. }
  78. const handleClose = () => {
  79. setAction(ScheduleAction.ENABLE)
  80. setScheduledDate('')
  81. setScheduledTime('')
  82. setReason('')
  83. setNotes('')
  84. onClose()
  85. }
  86. // Get tomorrow's date as minimum
  87. const getTomorrowDate = () => {
  88. const tomorrow = new Date()
  89. tomorrow.setDate(tomorrow.getDate() + 1)
  90. return tomorrow.toISOString().split('T')[0]
  91. }
  92. const getActionLabel = (actionType: ScheduleAction) => {
  93. const labels = {
  94. [ScheduleAction.ENABLE]: '启用',
  95. [ScheduleAction.DISABLE]: '禁用',
  96. [ScheduleAction.ARCHIVE]: '归档',
  97. }
  98. return labels[actionType]
  99. }
  100. return (
  101. <Modal
  102. isShow={isOpen}
  103. onClose={handleClose}
  104. title={
  105. <div className="flex items-center gap-2">
  106. <RiCalendarScheduleLine className="w-5 h-5" />
  107. <span>创建定时任务</span>
  108. </div>
  109. }
  110. className="!w-[540px]"
  111. >
  112. <div className="space-y-4 py-4">
  113. {/* Document Name */}
  114. <div>
  115. <label className="text-text-secondary text-sm font-medium mb-2 block">
  116. 文档
  117. </label>
  118. <div className="px-3 py-2 bg-background-section-burn rounded-lg text-sm text-text-tertiary">
  119. {documentName}
  120. </div>
  121. </div>
  122. {/* Action Selection */}
  123. <div>
  124. <label className="text-text-secondary text-sm font-medium mb-2 block">
  125. 操作类型
  126. </label>
  127. <div className="grid grid-cols-3 gap-2">
  128. {[
  129. { value: ScheduleAction.ENABLE, label: '启用' },
  130. { value: ScheduleAction.DISABLE, label: '禁用' },
  131. { value: ScheduleAction.ARCHIVE, label: '归档' },
  132. ].map(option => (
  133. <button
  134. key={option.value}
  135. type="button"
  136. onClick={() => setAction(option.value)}
  137. className={`
  138. px-4 py-2 rounded-lg border text-sm font-medium transition-colors
  139. ${action === option.value
  140. ? 'border-components-button-primary-bg bg-components-button-primary-bg-hover text-components-button-primary-text'
  141. : 'border-divider-regular bg-components-button-secondary-bg text-components-button-secondary-text hover:bg-components-button-secondary-bg-hover'
  142. }
  143. `}
  144. >
  145. {option.label}
  146. </button>
  147. ))}
  148. </div>
  149. </div>
  150. {/* Scheduled Date & Time */}
  151. <div className="grid grid-cols-2 gap-3">
  152. <div>
  153. <label className="text-text-secondary text-sm font-medium mb-2 block">
  154. 日期
  155. </label>
  156. <input
  157. type="date"
  158. value={scheduledDate}
  159. onChange={e => setScheduledDate(e.target.value)}
  160. min={getTomorrowDate()}
  161. className="w-full px-3 py-2 bg-components-input-bg-normal border border-components-input-border-active rounded-lg text-sm text-text-primary placeholder:text-text-quaternary focus:outline-none focus:ring-1 focus:ring-components-input-border-active"
  162. />
  163. </div>
  164. <div>
  165. <label className="text-text-secondary text-sm font-medium mb-2 block">
  166. 时间
  167. </label>
  168. <input
  169. type="time"
  170. value={scheduledTime}
  171. onChange={e => setScheduledTime(e.target.value)}
  172. className="w-full px-3 py-2 bg-components-input-bg-normal border border-components-input-border-active rounded-lg text-sm text-text-primary placeholder:text-text-quaternary focus:outline-none focus:ring-1 focus:ring-components-input-border-active"
  173. />
  174. </div>
  175. </div>
  176. {/* Reason (Optional) */}
  177. <div>
  178. <label className="text-text-secondary text-sm font-medium mb-2 block">
  179. 原因 <span className="text-text-quaternary">(可选)</span>
  180. </label>
  181. <input
  182. type="text"
  183. value={reason}
  184. onChange={e => setReason(e.target.value)}
  185. placeholder="例如:每月内容更新"
  186. maxLength={100}
  187. className="w-full px-3 py-2 bg-components-input-bg-normal border border-components-input-border-active rounded-lg text-sm text-text-primary placeholder:text-text-quaternary focus:outline-none focus:ring-1 focus:ring-components-input-border-active"
  188. />
  189. </div>
  190. {/* Notes (Optional) */}
  191. <div>
  192. <label className="text-text-secondary text-sm font-medium mb-2 block">
  193. 备注 <span className="text-text-quaternary">(可选)</span>
  194. </label>
  195. <textarea
  196. value={notes}
  197. onChange={e => setNotes(e.target.value)}
  198. placeholder="附加信息..."
  199. maxLength={500}
  200. rows={3}
  201. className="w-full px-3 py-2 bg-components-input-bg-normal border border-components-input-border-active rounded-lg text-sm text-text-primary placeholder:text-text-quaternary focus:outline-none focus:ring-1 focus:ring-components-input-border-active resize-none"
  202. />
  203. </div>
  204. {/* Preview */}
  205. {scheduledDate && scheduledTime && (
  206. <div className="px-3 py-2 bg-background-section-burn rounded-lg flex items-start gap-2">
  207. <RiTimeLine className="w-4 h-4 text-text-accent mt-0.5 shrink-0" />
  208. <div className="text-sm">
  209. <div className="text-text-secondary font-medium">
  210. 将执行
  211. </div>
  212. <div className="text-text-tertiary mt-1">
  213. {getActionLabel(action)} {documentName} 于 {new Date(`${scheduledDate}T${scheduledTime}`).toLocaleString('zh-CN')}
  214. </div>
  215. </div>
  216. </div>
  217. )}
  218. </div>
  219. {/* Footer */}
  220. <div className="flex justify-end gap-2 pt-4 border-t border-divider-subtle">
  221. <Button
  222. onClick={handleClose}
  223. className="min-w-[80px]"
  224. >
  225. 取消
  226. </Button>
  227. <Button
  228. variant="primary"
  229. onClick={handleSubmit}
  230. disabled={isPending || !scheduledDate || !scheduledTime}
  231. loading={isPending}
  232. className="min-w-[80px]"
  233. >
  234. 创建
  235. </Button>
  236. </div>
  237. </Modal>
  238. )
  239. }
  240. export default ScheduleModal