| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327 |
- 'use client'
- import type { FC } from 'react'
- import { useState } from 'react'
- import {
- RiAddLine,
- RiCalendarScheduleLine,
- RiDeleteBinLine,
- RiRefreshLine,
- } from '@remixicon/react'
- import Modal from '@/app/components/base/modal'
- import Button from '@/app/components/base/button'
- import { ScheduleStatus } from '@/models/document-schedule'
- import type { Schedule } from '@/models/document-schedule'
- import {
- useCancelSchedule,
- useDocumentSchedules,
- useScheduleListKey,
- } from '@/service/knowledge/use-document-schedule'
- import { useToastContext } from '@/app/components/base/toast'
- import { useQueryClient } from '@tanstack/react-query'
- import Loading from '@/app/components/base/loading'
- import Confirm from '@/app/components/base/confirm'
- type ScheduleListProps = {
- isOpen: boolean
- onClose: () => void
- datasetId: string
- documentId: string
- documentName: string
- onCreateNew?: () => void
- }
- const ScheduleList: FC<ScheduleListProps> = ({
- isOpen,
- onClose,
- datasetId,
- documentId,
- documentName,
- onCreateNew,
- }) => {
- const { notify } = useToastContext()
- const queryClient = useQueryClient()
- const [includeCompleted, setIncludeCompleted] = useState(false)
- const [cancelScheduleId, setCancelScheduleId] = useState<string | null>(null)
- const { data, isLoading, refetch } = useDocumentSchedules(
- datasetId,
- documentId,
- includeCompleted,
- )
- const { mutateAsync: cancelSchedule, isPending: isCancelling } = useCancelSchedule()
- const handleCancelSchedule = async () => {
- if (!cancelScheduleId)
- return
- try {
- await cancelSchedule({
- datasetId,
- documentId,
- scheduleId: cancelScheduleId,
- })
- notify({
- type: 'success',
- message: '定时任务取消成功',
- })
- // Invalidate schedules query
- queryClient.invalidateQueries({
- queryKey: useScheduleListKey(datasetId, documentId),
- })
- setCancelScheduleId(null)
- }
- catch (error: any) {
- notify({
- type: 'error',
- message: error.message || '定时任务取消失败',
- })
- }
- }
- const getStatusBadge = (status: ScheduleStatus) => {
- const statusConfig = {
- [ScheduleStatus.PENDING]: {
- bg: 'bg-util-colors-blue-blue-100',
- text: 'text-util-colors-blue-blue-600',
- label: '待执行',
- },
- [ScheduleStatus.EXECUTING]: {
- bg: 'bg-util-colors-purple-purple-100',
- text: 'text-util-colors-purple-purple-600',
- label: '执行中',
- },
- [ScheduleStatus.COMPLETED]: {
- bg: 'bg-util-colors-teal-teal-100',
- text: 'text-util-colors-teal-teal-600',
- label: '已完成',
- },
- [ScheduleStatus.CANCELLED]: {
- bg: 'bg-util-colors-gray-gray-100',
- text: 'text-util-colors-gray-gray-600',
- label: '已取消',
- },
- [ScheduleStatus.FAILED]: {
- bg: 'bg-util-colors-red-red-100',
- text: 'text-util-colors-red-red-600',
- label: '失败',
- },
- }
- const config = statusConfig[status]
- return (
- <span className={`inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium ${config.bg} ${config.text}`}>
- {config.label}
- </span>
- )
- }
- const getActionBadge = (action: string) => {
- const actionConfig: Record<string, { color: string; label: string }> = {
- enable: { color: 'text-text-accent', label: '启用' },
- disable: { color: 'text-text-warning', label: '禁用' },
- archive: { color: 'text-text-tertiary', label: '归档' },
- }
- const config = actionConfig[action] || actionConfig.enable
- return (
- <span className={`font-medium ${config.color}`}>
- {config.label}
- </span>
- )
- }
- const schedules = data?.schedules || []
- const hasSchedules = schedules.length > 0
- return (
- <>
- <Modal
- isShow={isOpen}
- onClose={onClose}
- title={
- <div className="flex items-center gap-2">
- <RiCalendarScheduleLine className="w-5 h-5" />
- <span>定时任务列表</span>
- </div>
- }
- className="!w-[720px] !max-w-[90vw]"
- >
- <div className="py-4">
- {/* Document Name */}
- <div className="mb-4">
- <div className="text-sm text-text-tertiary">
- 文档: <span className="text-text-secondary font-medium">{documentName}</span>
- </div>
- </div>
- {/* Toolbar */}
- <div className="flex items-center justify-between mb-4">
- <label className="flex items-center gap-2 text-sm text-text-secondary cursor-pointer">
- <input
- type="checkbox"
- checked={includeCompleted}
- onChange={e => setIncludeCompleted(e.target.checked)}
- className="w-4 h-4 rounded border-components-checkbox-border text-components-checkbox-bg-checked focus:ring-2 focus:ring-components-checkbox-border-checked"
- />
- 显示已完成的任务
- </label>
- <div className="flex items-center gap-2">
- {onCreateNew && (
- <Button
- size="small"
- onClick={onCreateNew}
- className="gap-1"
- variant="primary"
- >
- <RiAddLine className="w-4 h-4" />
- 创建定时任务
- </Button>
- )}
- <Button
- size="small"
- onClick={() => refetch()}
- className="gap-1"
- >
- <RiRefreshLine className="w-4 h-4" />
- 刷新
- </Button>
- </div>
- </div>
- {/* Schedule List */}
- <div className="border border-divider-subtle rounded-lg overflow-hidden">
- {isLoading
- ? (
- <div className="flex items-center justify-center py-12">
- <Loading type="area" />
- </div>
- )
- : !hasSchedules
- ? (
- <div className="text-center py-12 text-text-quaternary">
- <RiCalendarScheduleLine className="w-12 h-12 mx-auto mb-3 opacity-50" />
- <div className="text-sm">暂无定时任务</div>
- {onCreateNew && (
- <Button
- size="small"
- onClick={onCreateNew}
- className="mt-4"
- variant="primary"
- >
- 创建第一个定时任务
- </Button>
- )}
- </div>
- )
- : (
- <div className="divide-y divide-divider-subtle max-h-[400px] overflow-y-auto">
- {schedules.map((schedule: Schedule) => (
- <div
- key={schedule.schedule_id}
- className="p-4 hover:bg-background-default-hover transition-colors"
- >
- <div className="flex items-start justify-between gap-4">
- <div className="flex-1 min-w-0">
- {/* Action and Status */}
- <div className="flex items-center gap-2 mb-2">
- {getActionBadge(schedule.action)}
- {getStatusBadge(schedule.status)}
- </div>
- {/* Scheduled Time */}
- <div className="text-sm text-text-secondary mb-1">
- <span className="text-text-tertiary">计划执行时间:</span>{' '}
- {new Date(schedule.scheduled_at).toLocaleString('zh-CN')}
- </div>
- {/* Metadata */}
- {schedule.metadata?.reason && (
- <div className="text-xs text-text-tertiary">
- <span className="font-medium">原因:</span> {schedule.metadata.reason}
- </div>
- )}
- {schedule.metadata?.notes && (
- <div className="text-xs text-text-tertiary mt-1">
- <span className="font-medium">备注:</span> {schedule.metadata.notes}
- </div>
- )}
- {/* Error Message */}
- {schedule.error_message && (
- <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">
- 错误: {schedule.error_message}
- </div>
- )}
- {/* Created/Completed/Cancelled Time */}
- <div className="text-xs text-text-quaternary mt-2">
- {schedule.status === ScheduleStatus.COMPLETED && schedule.completed_at && (
- <span>完成时间: {new Date(schedule.completed_at).toLocaleString('zh-CN')}</span>
- )}
- {schedule.status === ScheduleStatus.CANCELLED && schedule.cancelled_at && (
- <span>取消时间: {new Date(schedule.cancelled_at).toLocaleString('zh-CN')}</span>
- )}
- {schedule.status === ScheduleStatus.PENDING && (
- <span>创建时间: {new Date(schedule.created_at).toLocaleString('zh-CN')}</span>
- )}
- </div>
- </div>
- {/* Actions */}
- {schedule.status === ScheduleStatus.PENDING && (
- <Button
- size="small"
- onClick={() => setCancelScheduleId(schedule.schedule_id)}
- className="shrink-0 gap-1 text-util-colors-red-red-600 hover:text-util-colors-red-red-700"
- >
- <RiDeleteBinLine className="w-4 h-4" />
- 取消
- </Button>
- )}
- </div>
- </div>
- ))}
- </div>
- )}
- </div>
- {/* Summary */}
- {hasSchedules && (
- <div className="mt-4 text-sm text-text-quaternary">
- 共 {data?.total || 0} 个定时任务
- </div>
- )}
- </div>
- {/* Footer */}
- <div className="flex justify-end pt-4 border-t border-divider-subtle">
- <Button onClick={onClose}>
- 关闭
- </Button>
- </div>
- </Modal>
- {/* Cancel Confirmation */}
- {cancelScheduleId && (
- <Confirm
- isShow={!!cancelScheduleId}
- onClose={() => setCancelScheduleId(null)}
- onConfirm={handleCancelSchedule}
- onCancel={() => setCancelScheduleId(null)}
- title="取消定时任务"
- content="确定要取消这个定时任务吗?"
- confirmText="确定"
- isLoading={isCancelling}
- />
- )}
- </>
- )
- }
- export default ScheduleList
|