| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- 'use client'
- import type { FC } from 'react'
- import { useState } from 'react'
- import { RiCalendarScheduleLine, RiTimeLine } from '@remixicon/react'
- import Modal from '@/app/components/base/modal'
- import Button from '@/app/components/base/button'
- import { ScheduleAction } from '@/models/document-schedule'
- import type { CreateScheduleRequest } from '@/models/document-schedule'
- import { useCreateSchedule } from '@/service/knowledge/use-document-schedule'
- import { useToastContext } from '@/app/components/base/toast'
- import { useQueryClient } from '@tanstack/react-query'
- import { useScheduleListKey } from '@/service/knowledge/use-document-schedule'
- type ScheduleModalProps = {
- isOpen: boolean
- onClose: () => void
- datasetId: string
- documentId: string
- documentName: string
- }
- const ScheduleModal: FC<ScheduleModalProps> = ({
- isOpen,
- onClose,
- datasetId,
- documentId,
- documentName,
- }) => {
- const { notify } = useToastContext()
- const queryClient = useQueryClient()
- const [action, setAction] = useState<ScheduleAction>(ScheduleAction.ENABLE)
- const [scheduledDate, setScheduledDate] = useState('')
- const [scheduledTime, setScheduledTime] = useState('')
- const [reason, setReason] = useState('')
- const [notes, setNotes] = useState('')
- const { mutateAsync: createSchedule, isPending } = useCreateSchedule()
- const handleSubmit = async () => {
- if (!scheduledDate || !scheduledTime) {
- notify({ type: 'error', message: '请选择日期和时间' })
- return
- }
- // Combine date and time to ISO format
- const scheduledAt = new Date(`${scheduledDate}T${scheduledTime}`).toISOString()
- // Validate future time
- if (new Date(scheduledAt) <= new Date()) {
- notify({ type: 'error', message: '定时时间必须在未来' })
- return
- }
- const body: CreateScheduleRequest = {
- action,
- scheduled_at: scheduledAt,
- metadata: {
- reason: reason || undefined,
- notes: notes || undefined,
- },
- }
- try {
- await createSchedule({
- datasetId,
- documentId,
- body,
- })
- notify({
- type: 'success',
- message: '定时任务创建成功',
- })
- // Invalidate schedules query
- queryClient.invalidateQueries({
- queryKey: useScheduleListKey(datasetId, documentId),
- })
- handleClose()
- }
- catch (error: any) {
- notify({
- type: 'error',
- message: error.message || '定时任务创建失败',
- })
- }
- }
- const handleClose = () => {
- setAction(ScheduleAction.ENABLE)
- setScheduledDate('')
- setScheduledTime('')
- setReason('')
- setNotes('')
- onClose()
- }
- // Get tomorrow's date as minimum
- const getTomorrowDate = () => {
- const tomorrow = new Date()
- tomorrow.setDate(tomorrow.getDate() + 1)
- return tomorrow.toISOString().split('T')[0]
- }
- const getActionLabel = (actionType: ScheduleAction) => {
- const labels = {
- [ScheduleAction.ENABLE]: '启用',
- [ScheduleAction.DISABLE]: '禁用',
- [ScheduleAction.ARCHIVE]: '归档',
- }
- return labels[actionType]
- }
- return (
- <Modal
- isShow={isOpen}
- onClose={handleClose}
- title={
- <div className="flex items-center gap-2">
- <RiCalendarScheduleLine className="w-5 h-5" />
- <span>创建定时任务</span>
- </div>
- }
- className="!w-[540px]"
- >
- <div className="space-y-4 py-4">
- {/* Document Name */}
- <div>
- <label className="text-text-secondary text-sm font-medium mb-2 block">
- 文档
- </label>
- <div className="px-3 py-2 bg-background-section-burn rounded-lg text-sm text-text-tertiary">
- {documentName}
- </div>
- </div>
- {/* Action Selection */}
- <div>
- <label className="text-text-secondary text-sm font-medium mb-2 block">
- 操作类型
- </label>
- <div className="grid grid-cols-3 gap-2">
- {[
- { value: ScheduleAction.ENABLE, label: '启用' },
- { value: ScheduleAction.DISABLE, label: '禁用' },
- { value: ScheduleAction.ARCHIVE, label: '归档' },
- ].map(option => (
- <button
- key={option.value}
- type="button"
- onClick={() => setAction(option.value)}
- className={`
- px-4 py-2 rounded-lg border text-sm font-medium transition-colors
- ${action === option.value
- ? 'border-components-button-primary-bg bg-components-button-primary-bg-hover text-components-button-primary-text'
- : 'border-divider-regular bg-components-button-secondary-bg text-components-button-secondary-text hover:bg-components-button-secondary-bg-hover'
- }
- `}
- >
- {option.label}
- </button>
- ))}
- </div>
- </div>
- {/* Scheduled Date & Time */}
- <div className="grid grid-cols-2 gap-3">
- <div>
- <label className="text-text-secondary text-sm font-medium mb-2 block">
- 日期
- </label>
- <input
- type="date"
- value={scheduledDate}
- onChange={e => setScheduledDate(e.target.value)}
- min={getTomorrowDate()}
- 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"
- />
- </div>
- <div>
- <label className="text-text-secondary text-sm font-medium mb-2 block">
- 时间
- </label>
- <input
- type="time"
- value={scheduledTime}
- onChange={e => setScheduledTime(e.target.value)}
- 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"
- />
- </div>
- </div>
- {/* Reason (Optional) */}
- <div>
- <label className="text-text-secondary text-sm font-medium mb-2 block">
- 原因 <span className="text-text-quaternary">(可选)</span>
- </label>
- <input
- type="text"
- value={reason}
- onChange={e => setReason(e.target.value)}
- placeholder="例如:每月内容更新"
- maxLength={100}
- 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"
- />
- </div>
- {/* Notes (Optional) */}
- <div>
- <label className="text-text-secondary text-sm font-medium mb-2 block">
- 备注 <span className="text-text-quaternary">(可选)</span>
- </label>
- <textarea
- value={notes}
- onChange={e => setNotes(e.target.value)}
- placeholder="附加信息..."
- maxLength={500}
- rows={3}
- 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"
- />
- </div>
- {/* Preview */}
- {scheduledDate && scheduledTime && (
- <div className="px-3 py-2 bg-background-section-burn rounded-lg flex items-start gap-2">
- <RiTimeLine className="w-4 h-4 text-text-accent mt-0.5 shrink-0" />
- <div className="text-sm">
- <div className="text-text-secondary font-medium">
- 将执行
- </div>
- <div className="text-text-tertiary mt-1">
- {getActionLabel(action)} {documentName} 于 {new Date(`${scheduledDate}T${scheduledTime}`).toLocaleString('zh-CN')}
- </div>
- </div>
- </div>
- )}
- </div>
- {/* Footer */}
- <div className="flex justify-end gap-2 pt-4 border-t border-divider-subtle">
- <Button
- onClick={handleClose}
- className="min-w-[80px]"
- >
- 取消
- </Button>
- <Button
- variant="primary"
- onClick={handleSubmit}
- disabled={isPending || !scheduledDate || !scheduledTime}
- loading={isPending}
- className="min-w-[80px]"
- >
- 创建
- </Button>
- </div>
- </Modal>
- )
- }
- export default ScheduleModal
|