DeleteCourseModal.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { AlertTriangle, Loader2, Trash2, X } from 'lucide-react'
  2. import { FormEvent, useEffect, useState } from 'react'
  3. import { api } from '../api'
  4. import type { Course } from '../types'
  5. interface DeleteCourseModalProps {
  6. isOpen: boolean
  7. onClose: () => void
  8. course: Course
  9. onSuccess: () => void
  10. }
  11. export function DeleteCourseModal({
  12. isOpen,
  13. onClose,
  14. course,
  15. onSuccess
  16. }: DeleteCourseModalProps) {
  17. const [confirmName, setConfirmName] = useState('')
  18. const [busy, setBusy] = useState(false)
  19. const [error, setError] = useState('')
  20. useEffect(() => {
  21. if (isOpen) {
  22. setConfirmName('')
  23. setError('')
  24. setBusy(false)
  25. }
  26. }, [isOpen])
  27. // 监听 ESC 键关闭
  28. useEffect(() => {
  29. function handleKeyDown(e: KeyboardEvent) {
  30. if (e.key === 'Escape' && isOpen && !busy) {
  31. onClose()
  32. }
  33. }
  34. window.addEventListener('keydown', handleKeyDown)
  35. return () => window.removeEventListener('keydown', handleKeyDown)
  36. }, [isOpen, busy, onClose])
  37. if (!isOpen) return null
  38. const isMatch = confirmName.trim() === course.name.trim()
  39. async function handleSubmit(e: FormEvent) {
  40. e.preventDefault()
  41. if (!isMatch) {
  42. setError('输入的课程名称不一致,请核对后重试')
  43. return
  44. }
  45. setBusy(true)
  46. setError('')
  47. try {
  48. await api.deleteCourse(course.id)
  49. onSuccess()
  50. } catch (err) {
  51. setError((err as Error).message || '删除课程失败,请稍后重试')
  52. setBusy(false)
  53. }
  54. }
  55. return (
  56. <div
  57. className="modal-backdrop"
  58. onClick={(e) => {
  59. if (e.target === e.currentTarget && !busy) {
  60. onClose()
  61. }
  62. }}
  63. >
  64. <div className="modal-dialog delete-course-modal" role="dialog" aria-modal="true">
  65. <div className="modal-header">
  66. <div className="modal-header-simple-title">
  67. <h3>删除课程</h3>
  68. </div>
  69. <button
  70. type="button"
  71. className="close-modal-btn"
  72. onClick={onClose}
  73. disabled={busy}
  74. aria-label="关闭"
  75. >
  76. <X size={18} />
  77. </button>
  78. </div>
  79. <form onSubmit={handleSubmit} className="modal-form-content">
  80. <div className="delete-modal-body">
  81. <div className="danger-alert-card">
  82. <p>
  83. 此操作<strong>无法撤销</strong>。该课程及其关联的 <strong>{course.episodes?.length || 0} 个分集</strong>、<strong>{course.assets?.length || 0} 份素材文件</strong>和讲师信息都将被永久删除。
  84. </p>
  85. </div>
  86. <div className="form-group">
  87. <label className="form-label" htmlFor="delete-course-input">
  88. 请输入课程名称进行确认:
  89. </label>
  90. <div className="target-course-name-box">
  91. <code>{course.name}</code>
  92. </div>
  93. <input
  94. id="delete-course-input"
  95. type="text"
  96. autoFocus
  97. value={confirmName}
  98. onChange={(e) => {
  99. setConfirmName(e.target.value)
  100. if (error) setError('')
  101. }}
  102. placeholder="在此输入完整的课程名称"
  103. disabled={busy}
  104. className="delete-confirm-input"
  105. />
  106. </div>
  107. {error && <div className="notice notice-error">{error}</div>}
  108. </div>
  109. <div className="modal-footer">
  110. <button
  111. type="button"
  112. className="secondary-button"
  113. onClick={onClose}
  114. disabled={busy}
  115. >
  116. 取消
  117. </button>
  118. <button
  119. type="submit"
  120. className="danger-button danger-submit-btn"
  121. disabled={!isMatch || busy}
  122. >
  123. {busy ? (
  124. <>
  125. <Loader2 size={16} className="spin" />
  126. 正在删除…
  127. </>
  128. ) : (
  129. <>
  130. <Trash2 size={16} />
  131. 确认彻底删除
  132. </>
  133. )}
  134. </button>
  135. </div>
  136. </form>
  137. </div>
  138. </div>
  139. )
  140. }