CourseList.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <template>
  2. <div class="course-list-container">
  3. <h2 class="page-title">课程管理</h2>
  4. <el-card shadow="never" class="toolbar-card">
  5. <el-button type="primary" icon="Plus" @click="$router.push('/course/create')">新增课程</el-button>
  6. <el-button icon="Refresh" @click="loadList">刷新</el-button>
  7. </el-card>
  8. <el-card shadow="never" class="table-card">
  9. <el-table v-loading="loading" :data="list" stripe>
  10. <template #empty>
  11. <div class="custom-empty-state">
  12. <el-icon class="empty-icon"><Reading /></el-icon>
  13. <div class="empty-text">暂无课程数据</div>
  14. <div class="empty-hint">点击"新增课程"按钮开始创建</div>
  15. </div>
  16. </template>
  17. <el-table-column type="index" label="序号" width="60" />
  18. <el-table-column label="封面" width="100">
  19. <template #default="{ row }">
  20. <div v-if="row.coverImage || row.cover_image || row.cover" class="cover-wrapper">
  21. <el-image
  22. :src="row.coverImage || row.cover_image || row.cover"
  23. fit="cover"
  24. class="course-cover"
  25. :preview-src-list="[row.coverImage || row.cover_image || row.cover]"
  26. preview-teleported
  27. hide-on-click-modal
  28. >
  29. <template #error>
  30. <div class="image-slot">
  31. <el-icon><Picture /></el-icon>
  32. </div>
  33. </template>
  34. </el-image>
  35. </div>
  36. <div v-else class="image-slot">
  37. <el-icon><Picture /></el-icon>
  38. </div>
  39. </template>
  40. </el-table-column>
  41. <el-table-column prop="name" label="课程名称" min-width="200">
  42. <template #default="{ row }">
  43. <span class="data-highlight text-ellipsis-line" :title="row.name">
  44. {{ row.name }}
  45. </span>
  46. </template>
  47. </el-table-column>
  48. <el-table-column prop="categoryName" label="分类" width="120">
  49. <template #default="{ row }">
  50. <span class="data-meta">{{ row.categoryName || row.category_name || '-' }}</span>
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="instructor" label="讲师" width="120">
  54. <template #default="{ row }">
  55. <span class="data-meta">{{ row.instructor || row.teacher_name || row.teacherName || '-' }}</span>
  56. </template>
  57. </el-table-column>
  58. <el-table-column prop="price" label="价格" width="110" align="right">
  59. <template #default="{ row }">
  60. <span v-if="row.price > 0" class="amount-text">¥{{ row.price }}</span>
  61. <el-tag v-else class="status-success" size="small" effect="light">免费</el-tag>
  62. </template>
  63. </el-table-column>
  64. <el-table-column prop="duration" label="时长(分钟)" width="110" align="center">
  65. <template #default="{ row }">
  66. <span class="data-meta">{{ row.duration || '-' }}</span>
  67. </template>
  68. </el-table-column>
  69. <el-table-column prop="participants" label="报名人数" width="100" align="center">
  70. <template #default="{ row }">
  71. <span class="number-emphasis">{{ row.participants || 0 }}</span>
  72. </template>
  73. </el-table-column>
  74. <el-table-column prop="rating" label="评分" width="80" align="center">
  75. <template #default="{ row }">
  76. <span class="number-emphasis" style="color: var(--warning-color);">
  77. {{ row.rating || '-' }}
  78. </span>
  79. </template>
  80. </el-table-column>
  81. <el-table-column prop="isRecommended" label="推荐" width="90" align="center">
  82. <template #default="{ row }">
  83. <el-tag
  84. :class="row.isRecommended === 1 ? 'hot-tag' : 'status-pending'"
  85. size="small"
  86. effect="light"
  87. >
  88. {{ row.isRecommended === 1 ? '推荐' : '普通' }}
  89. </el-tag>
  90. </template>
  91. </el-table-column>
  92. <el-table-column prop="status" label="状态" width="90" align="center">
  93. <template #default="{ row }">
  94. <el-switch
  95. v-model="row.status"
  96. :active-value="1"
  97. :inactive-value="0"
  98. active-color="var(--success-color)"
  99. inactive-color="var(--border-dark)"
  100. @change="handleStatusChange(row)"
  101. />
  102. </template>
  103. </el-table-column>
  104. <el-table-column label="操作" width="220" fixed="right">
  105. <template #default="{ row }">
  106. <el-button type="primary" size="small" link @click="$router.push(`/course/detail/${row.id}`)">详情</el-button>
  107. <el-button type="primary" size="small" link @click="$router.push(`/course/edit/${row.id}`)">编辑</el-button>
  108. <el-button type="danger" size="small" link @click="handleDelete(row)">删除</el-button>
  109. </template>
  110. </el-table-column>
  111. </el-table>
  112. <div class="pagination-container">
  113. <el-pagination
  114. v-model:current-page="currentPage"
  115. v-model:page-size="pageSize"
  116. :total="total"
  117. layout="total, sizes, prev, pager, next"
  118. @size-change="loadList"
  119. @current-change="loadList"
  120. />
  121. </div>
  122. </el-card>
  123. </div>
  124. </template>
  125. <script setup>
  126. import { ref, onMounted } from 'vue'
  127. import { ElMessage, ElMessageBox } from 'element-plus'
  128. import { Picture, Reading } from '@element-plus/icons-vue'
  129. import request from '@/utils/request'
  130. import { API_ENDPOINTS } from '@/config/api'
  131. const loading = ref(false)
  132. const currentPage = ref(1)
  133. const pageSize = ref(10)
  134. const total = ref(0)
  135. const list = ref([])
  136. const loadList = async () => {
  137. loading.value = true
  138. try {
  139. const response = await request.get(API_ENDPOINTS.COURSE_LIST, {
  140. params: { page: currentPage.value, pageSize: pageSize.value }
  141. })
  142. if (response.code === 200) {
  143. // 后端返回格式: {list: [], total: xx}
  144. list.value = response.data.list || []
  145. total.value = response.data.total || 0
  146. }
  147. } catch (error) {
  148. console.error('加载课程列表失败:', error)
  149. ElMessage.error('加载课程列表失败')
  150. } finally {
  151. loading.value = false
  152. }
  153. }
  154. const handleStatusChange = async (row) => {
  155. if (!row || !row.id) {
  156. ElMessage.error('课程ID不存在,无法更新状态')
  157. return
  158. }
  159. try {
  160. const response = await request.put(`${API_ENDPOINTS.COURSE_UPDATE}/${row.id}`, {
  161. status: row.status
  162. })
  163. if (response.code === 200) {
  164. ElMessage.success('状态更新成功')
  165. } else {
  166. row.status = row.status === 1 ? 0 : 1
  167. ElMessage.error(response.message || '状态更新失败')
  168. }
  169. } catch (error) {
  170. console.error('状态更新失败:', error)
  171. row.status = row.status === 1 ? 0 : 1
  172. ElMessage.error('状态更新失败')
  173. }
  174. }
  175. const handleDelete = async (row) => {
  176. try {
  177. await ElMessageBox.confirm('确定要删除这个课程吗?', '提示', { type: 'warning' })
  178. const response = await request.delete(`${API_ENDPOINTS.COURSE_DELETE}/${row.id}`)
  179. if (response.code === 200) {
  180. ElMessage.success('删除成功')
  181. loadList()
  182. }
  183. } catch (error) {
  184. if (error !== 'cancel') console.error('删除失败:', error)
  185. }
  186. }
  187. onMounted(() => loadList())
  188. </script>
  189. <style scoped>
  190. @import '@/assets/list-common.css';
  191. .course-list-container {
  192. padding: 0;
  193. }
  194. .cover-wrapper {
  195. display: flex;
  196. align-items: center;
  197. justify-content: center;
  198. }
  199. .course-cover {
  200. width: 80px;
  201. height: 60px;
  202. border-radius: var(--radius-base);
  203. cursor: pointer;
  204. border: 2px solid var(--border-color);
  205. transition: all var(--transition-base);
  206. box-shadow: var(--shadow-sm);
  207. }
  208. .course-cover:hover {
  209. border-color: var(--primary-color);
  210. transform: scale(1.05);
  211. box-shadow: var(--shadow-md);
  212. }
  213. .image-slot {
  214. display: flex;
  215. align-items: center;
  216. justify-content: center;
  217. width: 80px;
  218. height: 60px;
  219. background-color: var(--bg-tertiary);
  220. color: var(--text-tertiary);
  221. font-size: 24px;
  222. border-radius: var(--radius-base);
  223. border: 1px solid var(--border-color);
  224. }
  225. </style>