| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import {
- Building2,
- Clock3,
- FileText,
- Filter,
- Layers,
- PackageCheck,
- Phone,
- RotateCw,
- Search,
- User,
- Users,
- } from 'lucide-react'
- import { useEffect, useState } from 'react'
- import { api } from '../../api'
- import { StatusBadge } from '../../components/StatusBadge'
- import { formatDate, formatDateTime } from '../../utils/date'
- import type { Course, CourseStatus } from '../../types'
- type StatusFilter = 'ALL' | CourseStatus
- export function AdminCourses() {
- const [courses, setCourses] = useState<Course[]>([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState('')
- const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL')
- const [keyword, setKeyword] = useState('')
- const loadCourses = () => {
- setLoading(true)
- api.admin
- .listCourses({ status: statusFilter, keyword })
- .then((r) => setCourses(r.courses))
- .catch((err) => setError(err.message))
- .finally(() => setLoading(false))
- }
- useEffect(() => {
- loadCourses()
- }, [statusFilter])
- const handleSearch = (e: React.FormEvent) => {
- e.preventDefault()
- loadCourses()
- }
- return (
- <div className="page admin-page">
- <div className="admin-header-row">
- <div>
- <h2>全部课程</h2>
- <p className="admin-subtitle">查看全平台课程及其当前状态</p>
- </div>
- <button className="secondary-button" onClick={loadCourses} title="刷新列表">
- <RotateCw size={15} /> 刷新
- </button>
- </div>
- {error && <div className="notice notice-error">{error}</div>}
- {/* 筛选与搜索工具条 */}
- <div className="admin-filter-bar">
- <div className="filter-tabs">
- <button
- className={`tab-item ${statusFilter === 'ALL' ? 'active' : ''}`}
- onClick={() => setStatusFilter('ALL')}
- >
- 全部课程
- </button>
- <button
- className={`tab-item ${statusFilter === 'WAITING_PRODUCTION' ? 'active' : ''}`}
- onClick={() => setStatusFilter('WAITING_PRODUCTION')}
- >
- 等待制作
- </button>
- <button
- className={`tab-item ${statusFilter === 'IN_PRODUCTION' ? 'active' : ''}`}
- onClick={() => setStatusFilter('IN_PRODUCTION')}
- >
- 正在制作
- </button>
- <button
- className={`tab-item ${statusFilter === 'COMPLETED' ? 'active' : ''}`}
- onClick={() => setStatusFilter('COMPLETED')}
- >
- 已完成交付
- </button>
- <button
- className={`tab-item ${statusFilter === 'DRAFT' ? 'active' : ''}`}
- onClick={() => setStatusFilter('DRAFT')}
- >
- 草稿中
- </button>
- </div>
- <form className="search-form" onSubmit={handleSearch}>
- <div className="input-with-icon">
- <Search size={16} />
- <input
- value={keyword}
- onChange={(e) => setKeyword(e.target.value)}
- placeholder="搜索课程名、用户手机或姓名…"
- />
- </div>
- <button type="submit" className="secondary-button">
- 搜索
- </button>
- </form>
- </div>
- {/* 课程列表 */}
- {loading ? (
- <div className="center-loading">正在查询课程…</div>
- ) : courses.length === 0 ? (
- <div className="empty-state">
- <Filter size={32} style={{ color: '#9aa3b3', marginBottom: 8 }} />
- <h3>没有找到符合条件的课程</h3>
- <p>尝试切换状态筛选条件或清空搜索关键字。</p>
- </div>
- ) : (
- <div className="admin-table-wrapper">
- <table className="admin-table">
- <thead>
- <tr>
- <th>课程名称与大纲</th>
- <th style={{ minWidth: 200 }}>提交制作用户信息</th>
- <th>素材 / 讲师 / 交付</th>
- <th>当前状态</th>
- <th>创建 / 更新时间</th>
- <th>提交 / 交付时间</th>
- </tr>
- </thead>
- <tbody>
- {courses.map((course) => (
- <tr key={course.id}>
- <td style={{ maxWidth: 260 }}>
- <div className="table-main-title">
- <strong className="course-table-name">{course.name}</strong>
- {course.description && (
- <small className="course-table-desc" title={course.description}>
- {course.description}
- </small>
- )}
- <div className="table-episodes-badge">
- <Layers size={11} /> 共 {course.episodes?.length || 1} 集
- </div>
- </div>
- </td>
- <td>
- {/* 醒目的提交用户信息单元格 */}
- <div className="user-highlight-cell">
- <div className="user-avatar-mini">
- {(course.creator?.contactName || course.creator?.phone || '用')[0]}
- </div>
- <div className="user-info-stack">
- <strong className="user-contact-name">
- {course.creator?.contactName || '未填写联系人'}
- </strong>
- <span className="user-org-text" title={course.organizationName || '个人工作区'}>
- <Building2 size={12} /> {course.organizationName || '个人工作区'}
- </span>
- <span className="user-phone-text">
- <Phone size={12} /> {course.creator?.phone || '无手机号'}
- </span>
- </div>
- </div>
- </td>
- <td>
- <div className="badge-group">
- <span className="badge-count">
- <FileText size={12} /> {course.assets?.length || 0} 资产
- </span>
- <span className="badge-count">
- <Users size={12} /> {course.instructors?.length || 0} 讲师
- </span>
- {course.deliverables && course.deliverables.length > 0 && (
- <span className="badge-count deliverable-badge">
- <PackageCheck size={12} /> {course.deliverables.length} 交付物
- </span>
- )}
- </div>
- </td>
- <td>
- <StatusBadge status={course.status} />
- </td>
- <td>
- <div className="time-cell">
- <small title={`创建时间:${formatDateTime(course.createdAt)}`}>
- 建: {formatDate(course.createdAt)}
- </small>
- <small title={`最后更新时间:${formatDateTime(course.updatedAt || course.createdAt)}`} style={{ color: '#0f172a', fontWeight: 500 }}>
- 更: {formatDateTime(course.updatedAt || course.createdAt)}
- </small>
- </div>
- </td>
- <td>
- <div className="time-cell">
- <small title="提交时间">
- 提: {course.submittedAt ? new Date(course.submittedAt).toLocaleDateString('zh-CN') : '未提交'}
- </small>
- {course.completedAt && (
- <small title="交付时间" style={{ color: '#2e7d32', fontWeight: 500 }}>
- 成: {new Date(course.completedAt).toLocaleDateString('zh-CN')}
- </small>
- )}
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- )}
- </div>
- )
- }
|