schedule-list.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. 'use client'
  2. import type { FC } from 'react'
  3. import { useState } from 'react'
  4. import {
  5. RiAddLine,
  6. RiCalendarScheduleLine,
  7. RiDeleteBinLine,
  8. RiRefreshLine,
  9. } from '@remixicon/react'
  10. import Modal from '@/app/components/base/modal'
  11. import Button from '@/app/components/base/button'
  12. import { ScheduleStatus } from '@/models/document-schedule'
  13. import type { Schedule } from '@/models/document-schedule'
  14. import {
  15. useCancelSchedule,
  16. useDocumentSchedules,
  17. useScheduleListKey,
  18. } from '@/service/knowledge/use-document-schedule'
  19. import { useToastContext } from '@/app/components/base/toast'
  20. import { useQueryClient } from '@tanstack/react-query'
  21. import Loading from '@/app/components/base/loading'
  22. import Confirm from '@/app/components/base/confirm'
  23. type ScheduleListProps = {
  24. isOpen: boolean
  25. onClose: () => void
  26. datasetId: string
  27. documentId: string
  28. documentName: string
  29. onCreateNew?: () => void
  30. }
  31. const ScheduleList: FC<ScheduleListProps> = ({
  32. isOpen,
  33. onClose,
  34. datasetId,
  35. documentId,
  36. documentName,
  37. onCreateNew,
  38. }) => {
  39. const { notify } = useToastContext()
  40. const queryClient = useQueryClient()
  41. const [includeCompleted, setIncludeCompleted] = useState(false)
  42. const [cancelScheduleId, setCancelScheduleId] = useState<string | null>(null)
  43. const { data, isLoading, refetch } = useDocumentSchedules(
  44. datasetId,
  45. documentId,
  46. includeCompleted,
  47. )
  48. const { mutateAsync: cancelSchedule, isPending: isCancelling } = useCancelSchedule()
  49. const handleCancelSchedule = async () => {
  50. if (!cancelScheduleId)
  51. return
  52. try {
  53. await cancelSchedule({
  54. datasetId,
  55. documentId,
  56. scheduleId: cancelScheduleId,
  57. })
  58. notify({
  59. type: 'success',
  60. message: '定时任务取消成功',
  61. })
  62. // Invalidate schedules query
  63. queryClient.invalidateQueries({
  64. queryKey: useScheduleListKey(datasetId, documentId),
  65. })
  66. setCancelScheduleId(null)
  67. }
  68. catch (error: any) {
  69. notify({
  70. type: 'error',
  71. message: error.message || '定时任务取消失败',
  72. })
  73. }
  74. }
  75. const getStatusBadge = (status: ScheduleStatus) => {
  76. const statusConfig = {
  77. [ScheduleStatus.PENDING]: {
  78. bg: 'bg-util-colors-blue-blue-100',
  79. text: 'text-util-colors-blue-blue-600',
  80. label: '待执行',
  81. },
  82. [ScheduleStatus.EXECUTING]: {
  83. bg: 'bg-util-colors-purple-purple-100',
  84. text: 'text-util-colors-purple-purple-600',
  85. label: '执行中',
  86. },
  87. [ScheduleStatus.COMPLETED]: {
  88. bg: 'bg-util-colors-teal-teal-100',
  89. text: 'text-util-colors-teal-teal-600',
  90. label: '已完成',
  91. },
  92. [ScheduleStatus.CANCELLED]: {
  93. bg: 'bg-util-colors-gray-gray-100',
  94. text: 'text-util-colors-gray-gray-600',
  95. label: '已取消',
  96. },
  97. [ScheduleStatus.FAILED]: {
  98. bg: 'bg-util-colors-red-red-100',
  99. text: 'text-util-colors-red-red-600',
  100. label: '失败',
  101. },
  102. }
  103. const config = statusConfig[status]
  104. return (
  105. <span className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium ${config.bg} ${config.text}`}>
  106. {config.label}
  107. </span>
  108. )
  109. }
  110. const getActionBadge = (action: string) => {
  111. const actionConfig: Record<string, { color: string; label: string }> = {
  112. enable: { color: 'text-text-accent', label: '启用' },
  113. disable: { color: 'text-text-warning', label: '禁用' },
  114. archive: { color: 'text-text-tertiary', label: '归档' },
  115. }
  116. const config = actionConfig[action] || actionConfig.enable
  117. return (
  118. <span className={`font-medium ${config.color}`}>
  119. {config.label}
  120. </span>
  121. )
  122. }
  123. const schedules = data?.schedules || []
  124. const hasSchedules = schedules.length > 0
  125. return (
  126. <>
  127. <Modal
  128. isShow={isOpen}
  129. onClose={onClose}
  130. title={
  131. <div className="flex items-center gap-2">
  132. <RiCalendarScheduleLine className="w-5 h-5" />
  133. <span>定时任务列表</span>
  134. </div>
  135. }
  136. className="!w-[720px] !max-w-[90vw]"
  137. >
  138. <div className="py-4">
  139. {/* Document Name */}
  140. <div className="mb-4">
  141. <div className="text-sm text-text-tertiary">
  142. 文档: <span className="text-text-secondary font-medium">{documentName}</span>
  143. </div>
  144. </div>
  145. {/* Toolbar */}
  146. <div className="flex items-center justify-between mb-4">
  147. <label className="flex items-center gap-2 text-sm text-text-secondary cursor-pointer">
  148. <input
  149. type="checkbox"
  150. checked={includeCompleted}
  151. onChange={e => setIncludeCompleted(e.target.checked)}
  152. className="w-4 h-4 rounded border-components-checkbox-border text-components-checkbox-bg-checked focus:ring-2 focus:ring-components-checkbox-border-checked"
  153. />
  154. 显示已完成的任务
  155. </label>
  156. <div className="flex items-center gap-2">
  157. {onCreateNew && (
  158. <Button
  159. size="small"
  160. onClick={onCreateNew}
  161. className="gap-1"
  162. variant="primary"
  163. >
  164. <RiAddLine className="w-4 h-4" />
  165. 创建定时任务
  166. </Button>
  167. )}
  168. <Button
  169. size="small"
  170. onClick={() => refetch()}
  171. className="gap-1"
  172. >
  173. <RiRefreshLine className="w-4 h-4" />
  174. 刷新
  175. </Button>
  176. </div>
  177. </div>
  178. {/* Schedule List */}
  179. <div className="border border-divider-subtle rounded-lg overflow-hidden">
  180. {isLoading
  181. ? (
  182. <div className="flex items-center justify-center py-12">
  183. <Loading type="area" />
  184. </div>
  185. )
  186. : !hasSchedules
  187. ? (
  188. <div className="text-center py-12 text-text-quaternary">
  189. <RiCalendarScheduleLine className="w-12 h-12 mx-auto mb-3 opacity-50" />
  190. <div className="text-sm">暂无定时任务</div>
  191. {onCreateNew && (
  192. <Button
  193. size="small"
  194. onClick={onCreateNew}
  195. className="mt-4"
  196. variant="primary"
  197. >
  198. 创建第一个定时任务
  199. </Button>
  200. )}
  201. </div>
  202. )
  203. : (
  204. <div className="divide-y divide-divider-subtle max-h-[400px] overflow-y-auto">
  205. {schedules.map((schedule: Schedule) => (
  206. <div
  207. key={schedule.schedule_id}
  208. className="p-4 hover:bg-background-default-hover transition-colors"
  209. >
  210. <div className="flex items-start justify-between gap-4">
  211. <div className="flex-1 min-w-0">
  212. {/* Action and Status */}
  213. <div className="flex items-center gap-2 mb-2">
  214. {getActionBadge(schedule.action)}
  215. {getStatusBadge(schedule.status)}
  216. </div>
  217. {/* Scheduled Time */}
  218. <div className="text-sm text-text-secondary mb-1">
  219. <span className="text-text-tertiary">计划执行时间:</span>{' '}
  220. {new Date(schedule.scheduled_at).toLocaleString('zh-CN')}
  221. </div>
  222. {/* Metadata */}
  223. {schedule.metadata?.reason && (
  224. <div className="text-xs text-text-tertiary">
  225. <span className="font-medium">原因:</span> {schedule.metadata.reason}
  226. </div>
  227. )}
  228. {schedule.metadata?.notes && (
  229. <div className="text-xs text-text-tertiary mt-1">
  230. <span className="font-medium">备注:</span> {schedule.metadata.notes}
  231. </div>
  232. )}
  233. {/* Error Message */}
  234. {schedule.error_message && (
  235. <div className="mt-2 px-2 py-1 bg-util-colors-red-red-50 border border-util-colors-red-red-200 rounded text-xs text-util-colors-red-red-600">
  236. 错误: {schedule.error_message}
  237. </div>
  238. )}
  239. {/* Created/Completed/Cancelled Time */}
  240. <div className="text-xs text-text-quaternary mt-2">
  241. {schedule.status === ScheduleStatus.COMPLETED && schedule.completed_at && (
  242. <span>完成时间: {new Date(schedule.completed_at).toLocaleString('zh-CN')}</span>
  243. )}
  244. {schedule.status === ScheduleStatus.CANCELLED && schedule.cancelled_at && (
  245. <span>取消时间: {new Date(schedule.cancelled_at).toLocaleString('zh-CN')}</span>
  246. )}
  247. {schedule.status === ScheduleStatus.PENDING && (
  248. <span>创建时间: {new Date(schedule.created_at).toLocaleString('zh-CN')}</span>
  249. )}
  250. </div>
  251. </div>
  252. {/* Actions */}
  253. {schedule.status === ScheduleStatus.PENDING && (
  254. <Button
  255. size="small"
  256. onClick={() => setCancelScheduleId(schedule.schedule_id)}
  257. className="shrink-0 gap-1 text-util-colors-red-red-600 hover:text-util-colors-red-red-700"
  258. >
  259. <RiDeleteBinLine className="w-4 h-4" />
  260. 取消
  261. </Button>
  262. )}
  263. </div>
  264. </div>
  265. ))}
  266. </div>
  267. )}
  268. </div>
  269. {/* Summary */}
  270. {hasSchedules && (
  271. <div className="mt-4 text-sm text-text-quaternary">
  272. 共 {data?.total || 0} 个定时任务
  273. </div>
  274. )}
  275. </div>
  276. {/* Footer */}
  277. <div className="flex justify-end pt-4 border-t border-divider-subtle">
  278. <Button onClick={onClose}>
  279. 关闭
  280. </Button>
  281. </div>
  282. </Modal>
  283. {/* Cancel Confirmation */}
  284. {cancelScheduleId && (
  285. <Confirm
  286. isShow={!!cancelScheduleId}
  287. onClose={() => setCancelScheduleId(null)}
  288. onConfirm={handleCancelSchedule}
  289. onCancel={() => setCancelScheduleId(null)}
  290. title="取消定时任务"
  291. content="确定要取消这个定时任务吗?"
  292. confirmText="确定"
  293. isLoading={isCancelling}
  294. />
  295. )}
  296. </>
  297. )
  298. }
  299. export default ScheduleList