AdminCourses.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {
  2. Building2,
  3. Clock3,
  4. FileText,
  5. Filter,
  6. Layers,
  7. PackageCheck,
  8. Phone,
  9. RotateCw,
  10. Search,
  11. User,
  12. Users,
  13. } from 'lucide-react'
  14. import { useEffect, useState } from 'react'
  15. import { api } from '../../api'
  16. import { StatusBadge } from '../../components/StatusBadge'
  17. import { formatDate, formatDateTime } from '../../utils/date'
  18. import type { Course, CourseStatus } from '../../types'
  19. type StatusFilter = 'ALL' | CourseStatus
  20. export function AdminCourses() {
  21. const [courses, setCourses] = useState<Course[]>([])
  22. const [loading, setLoading] = useState(true)
  23. const [error, setError] = useState('')
  24. const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL')
  25. const [keyword, setKeyword] = useState('')
  26. const loadCourses = () => {
  27. setLoading(true)
  28. api.admin
  29. .listCourses({ status: statusFilter, keyword })
  30. .then((r) => setCourses(r.courses))
  31. .catch((err) => setError(err.message))
  32. .finally(() => setLoading(false))
  33. }
  34. useEffect(() => {
  35. loadCourses()
  36. }, [statusFilter])
  37. const handleSearch = (e: React.FormEvent) => {
  38. e.preventDefault()
  39. loadCourses()
  40. }
  41. return (
  42. <div className="page admin-page">
  43. <div className="admin-header-row">
  44. <div>
  45. <h2>全部课程</h2>
  46. <p className="admin-subtitle">查看全平台课程及其当前状态</p>
  47. </div>
  48. <button className="secondary-button" onClick={loadCourses} title="刷新列表">
  49. <RotateCw size={15} /> 刷新
  50. </button>
  51. </div>
  52. {error && <div className="notice notice-error">{error}</div>}
  53. {/* 筛选与搜索工具条 */}
  54. <div className="admin-filter-bar">
  55. <div className="filter-tabs">
  56. <button
  57. className={`tab-item ${statusFilter === 'ALL' ? 'active' : ''}`}
  58. onClick={() => setStatusFilter('ALL')}
  59. >
  60. 全部课程
  61. </button>
  62. <button
  63. className={`tab-item ${statusFilter === 'WAITING_PRODUCTION' ? 'active' : ''}`}
  64. onClick={() => setStatusFilter('WAITING_PRODUCTION')}
  65. >
  66. 等待制作
  67. </button>
  68. <button
  69. className={`tab-item ${statusFilter === 'IN_PRODUCTION' ? 'active' : ''}`}
  70. onClick={() => setStatusFilter('IN_PRODUCTION')}
  71. >
  72. 正在制作
  73. </button>
  74. <button
  75. className={`tab-item ${statusFilter === 'COMPLETED' ? 'active' : ''}`}
  76. onClick={() => setStatusFilter('COMPLETED')}
  77. >
  78. 已完成交付
  79. </button>
  80. <button
  81. className={`tab-item ${statusFilter === 'DRAFT' ? 'active' : ''}`}
  82. onClick={() => setStatusFilter('DRAFT')}
  83. >
  84. 草稿中
  85. </button>
  86. </div>
  87. <form className="search-form" onSubmit={handleSearch}>
  88. <div className="input-with-icon">
  89. <Search size={16} />
  90. <input
  91. value={keyword}
  92. onChange={(e) => setKeyword(e.target.value)}
  93. placeholder="搜索课程名、用户手机或姓名…"
  94. />
  95. </div>
  96. <button type="submit" className="secondary-button">
  97. 搜索
  98. </button>
  99. </form>
  100. </div>
  101. {/* 课程列表 */}
  102. {loading ? (
  103. <div className="center-loading">正在查询课程…</div>
  104. ) : courses.length === 0 ? (
  105. <div className="empty-state">
  106. <Filter size={32} style={{ color: '#9aa3b3', marginBottom: 8 }} />
  107. <h3>没有找到符合条件的课程</h3>
  108. <p>尝试切换状态筛选条件或清空搜索关键字。</p>
  109. </div>
  110. ) : (
  111. <div className="admin-table-wrapper">
  112. <table className="admin-table">
  113. <thead>
  114. <tr>
  115. <th>课程名称与大纲</th>
  116. <th style={{ minWidth: 200 }}>提交制作用户信息</th>
  117. <th>素材 / 讲师 / 交付</th>
  118. <th>当前状态</th>
  119. <th>创建 / 更新时间</th>
  120. <th>提交 / 交付时间</th>
  121. </tr>
  122. </thead>
  123. <tbody>
  124. {courses.map((course) => (
  125. <tr key={course.id}>
  126. <td style={{ maxWidth: 260 }}>
  127. <div className="table-main-title">
  128. <strong className="course-table-name">{course.name}</strong>
  129. {course.description && (
  130. <small className="course-table-desc" title={course.description}>
  131. {course.description}
  132. </small>
  133. )}
  134. <div className="table-episodes-badge">
  135. <Layers size={11} /> 共 {course.episodes?.length || 1} 集
  136. </div>
  137. </div>
  138. </td>
  139. <td>
  140. {/* 醒目的提交用户信息单元格 */}
  141. <div className="user-highlight-cell">
  142. <div className="user-avatar-mini">
  143. {(course.creator?.contactName || course.creator?.phone || '用')[0]}
  144. </div>
  145. <div className="user-info-stack">
  146. <strong className="user-contact-name">
  147. {course.creator?.contactName || '未填写联系人'}
  148. </strong>
  149. <span className="user-org-text" title={course.organizationName || '个人工作区'}>
  150. <Building2 size={12} /> {course.organizationName || '个人工作区'}
  151. </span>
  152. <span className="user-phone-text">
  153. <Phone size={12} /> {course.creator?.phone || '无手机号'}
  154. </span>
  155. </div>
  156. </div>
  157. </td>
  158. <td>
  159. <div className="badge-group">
  160. <span className="badge-count">
  161. <FileText size={12} /> {course.assets?.length || 0} 资产
  162. </span>
  163. <span className="badge-count">
  164. <Users size={12} /> {course.instructors?.length || 0} 讲师
  165. </span>
  166. {course.deliverables && course.deliverables.length > 0 && (
  167. <span className="badge-count deliverable-badge">
  168. <PackageCheck size={12} /> {course.deliverables.length} 交付物
  169. </span>
  170. )}
  171. </div>
  172. </td>
  173. <td>
  174. <StatusBadge status={course.status} />
  175. </td>
  176. <td>
  177. <div className="time-cell">
  178. <small title={`创建时间:${formatDateTime(course.createdAt)}`}>
  179. 建: {formatDate(course.createdAt)}
  180. </small>
  181. <small title={`最后更新时间:${formatDateTime(course.updatedAt || course.createdAt)}`} style={{ color: '#0f172a', fontWeight: 500 }}>
  182. 更: {formatDateTime(course.updatedAt || course.createdAt)}
  183. </small>
  184. </div>
  185. </td>
  186. <td>
  187. <div className="time-cell">
  188. <small title="提交时间">
  189. 提: {course.submittedAt ? new Date(course.submittedAt).toLocaleDateString('zh-CN') : '未提交'}
  190. </small>
  191. {course.completedAt && (
  192. <small title="交付时间" style={{ color: '#2e7d32', fontWeight: 500 }}>
  193. 成: {new Date(course.completedAt).toLocaleDateString('zh-CN')}
  194. </small>
  195. )}
  196. </div>
  197. </td>
  198. </tr>
  199. ))}
  200. </tbody>
  201. </table>
  202. </div>
  203. )}
  204. </div>
  205. )
  206. }