CourseForm.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <template>
  2. <div class="course-form-container">
  3. <h2 class="page-title">{{ isEdit ? '编辑课程' : '创建课程' }}</h2>
  4. <el-card shadow="never">
  5. <el-form
  6. ref="formRef"
  7. :model="form"
  8. :rules="rules"
  9. label-width="120px"
  10. scroll-to-error
  11. status-icon
  12. >
  13. <el-form-item label="课程名称" prop="name">
  14. <el-input v-model="form.name" placeholder="请输入课程名称" />
  15. </el-form-item>
  16. <el-row :gutter="20">
  17. <el-col :span="12">
  18. <el-form-item label="课程分类" prop="categoryName">
  19. <el-select v-model="form.categoryName" placeholder="请选择课程分类" style="width: 100%">
  20. <el-option label="情感沟通" value="情感沟通" />
  21. <el-option label="恋爱技巧" value="恋爱技巧" />
  22. <el-option label="婚姻经营" value="婚姻经营" />
  23. <el-option label="情感修复" value="情感修复" />
  24. <el-option label="亲密关系" value="亲密关系" />
  25. </el-select>
  26. </el-form-item>
  27. </el-col>
  28. <el-col :span="12">
  29. <el-form-item label="讲师" prop="instructor">
  30. <el-input v-model="form.instructor" placeholder="请输入讲师姓名" />
  31. </el-form-item>
  32. </el-col>
  33. </el-row>
  34. <el-row :gutter="20">
  35. <el-col :span="12">
  36. <el-form-item label="课程时长" prop="duration">
  37. <el-input-number v-model="form.duration" :min="0" placeholder="分钟" style="width: 100%" />
  38. <span style="margin-left: 10px; color: #999;">分钟</span>
  39. </el-form-item>
  40. </el-col>
  41. <el-col :span="12">
  42. <el-form-item label="课程价格" prop="price">
  43. <el-input-number v-model="form.price" :min="0" :precision="2" style="width: 100%" />
  44. <span style="margin-left: 10px; color: #999;">元(0表示免费)</span>
  45. </el-form-item>
  46. </el-col>
  47. </el-row>
  48. <el-row :gutter="20">
  49. <el-col :span="12">
  50. <el-form-item label="课程评分" prop="rating">
  51. <el-rate v-model="form.rating" :max="5" allow-half />
  52. </el-form-item>
  53. </el-col>
  54. <el-col :span="12">
  55. <el-form-item label="是否推荐" prop="isRecommended">
  56. <el-switch v-model="form.isRecommended" :active-value="1" :inactive-value="0" />
  57. </el-form-item>
  58. </el-col>
  59. </el-row>
  60. <el-form-item label="课程封面" prop="coverImage">
  61. <div class="cover-upload-wrapper">
  62. <el-upload
  63. class="cover-uploader"
  64. :http-request="handleUpload"
  65. :show-file-list="false"
  66. :before-upload="beforeUpload"
  67. accept="image/*"
  68. >
  69. <img v-if="form.coverImage" :src="form.coverImage" class="cover-preview" />
  70. <el-icon v-else class="cover-uploader-icon"><Plus /></el-icon>
  71. </el-upload>
  72. <div class="cover-tips">
  73. <p>建议尺寸:750x420 像素,支持 JPG、PNG 格式,文件大小不超过 2MB</p>
  74. <el-button v-if="form.coverImage" size="small" type="danger" text @click="removeCover">删除封面</el-button>
  75. </div>
  76. </div>
  77. </el-form-item>
  78. <el-form-item label="课程描述" prop="description">
  79. <el-input v-model="form.description" type="textarea" :rows="4" placeholder="请输入课程简介" />
  80. </el-form-item>
  81. <el-form-item label="课程内容" prop="content">
  82. <el-input v-model="form.content" type="textarea" :rows="8" placeholder="请输入课程详细内容" />
  83. </el-form-item>
  84. <el-form-item label="状态" prop="status">
  85. <el-radio-group v-model="form.status">
  86. <el-radio :label="1">上架</el-radio>
  87. <el-radio :label="0">下架</el-radio>
  88. </el-radio-group>
  89. </el-form-item>
  90. <el-form-item>
  91. <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
  92. {{ isEdit ? '保存修改' : '创建课程' }}
  93. </el-button>
  94. <el-button @click="$router.back()">取消</el-button>
  95. </el-form-item>
  96. </el-form>
  97. </el-card>
  98. </div>
  99. </template>
  100. <script setup>
  101. import { ref, reactive, onMounted, computed } from 'vue'
  102. import { useRoute, useRouter } from 'vue-router'
  103. import { ElMessage } from 'element-plus'
  104. import { Plus } from '@element-plus/icons-vue'
  105. import request from '@/utils/request'
  106. import { API_ENDPOINTS } from '@/config/api'
  107. const route = useRoute()
  108. const router = useRouter()
  109. const isEdit = ref(false)
  110. const submitLoading = ref(false)
  111. const formRef = ref(null)
  112. const form = reactive({
  113. id: null,
  114. name: '',
  115. categoryName: '',
  116. instructor: '',
  117. duration: 0,
  118. price: 0,
  119. rating: 4.5,
  120. isRecommended: 0,
  121. coverImage: '',
  122. description: '',
  123. content: '',
  124. status: 1
  125. })
  126. const rules = {
  127. name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
  128. categoryName: [{ required: true, message: '请选择课程分类', trigger: 'change' }],
  129. instructor: [{ required: true, message: '请输入讲师姓名', trigger: 'blur' }],
  130. duration: [{ required: true, message: '请输入课程时长', trigger: 'blur' }],
  131. description: [{ required: true, message: '请输入课程描述', trigger: 'blur' }]
  132. }
  133. // 上传前检查
  134. const beforeUpload = (file) => {
  135. const isImage = file.type.startsWith('image/')
  136. const isLt2M = file.size / 1024 / 1024 < 2
  137. if (!isImage) {
  138. ElMessage.error('只能上传图片文件!')
  139. return false
  140. }
  141. if (!isLt2M) {
  142. ElMessage.error('图片大小不能超过 2MB!')
  143. return false
  144. }
  145. return true
  146. }
  147. // 上传图片
  148. const handleUpload = async (options) => {
  149. const formData = new FormData()
  150. formData.append('file', options.file)
  151. try {
  152. const response = await request.post(API_ENDPOINTS.UPLOAD_IMAGE, formData, {
  153. headers: { 'Content-Type': 'multipart/form-data' }
  154. })
  155. if (response.code === 200) {
  156. // 处理不同的返回格式
  157. let imageUrl = ''
  158. if (typeof response.data === 'string') {
  159. imageUrl = response.data
  160. } else if (response.data && response.data.url) {
  161. imageUrl = response.data.url
  162. } else if (response.data && response.data.path) {
  163. imageUrl = response.data.path
  164. } else {
  165. imageUrl = String(response.data || '')
  166. }
  167. form.coverImage = imageUrl
  168. ElMessage.success('封面上传成功')
  169. } else {
  170. ElMessage.error(response.message || '封面上传失败')
  171. }
  172. } catch (error) {
  173. console.error('上传异常:', error)
  174. ElMessage.error('封面上传失败,请重试')
  175. }
  176. }
  177. // 删除封面
  178. const removeCover = () => {
  179. form.coverImage = ''
  180. ElMessage.success('封面已删除')
  181. }
  182. const loadDetail = async (id) => {
  183. try {
  184. const response = await request.get(`${API_ENDPOINTS.COURSE_DETAIL}/${id}`)
  185. if (response.code === 200) {
  186. const data = response.data
  187. // 映射API返回的下划线字段到表单的驼峰字段
  188. Object.assign(form, {
  189. id: data.id,
  190. name: data.name || '',
  191. categoryName: data.categoryName || data.category_name || '',
  192. instructor: data.instructor || data.teacher_name || data.teacherName || '',
  193. duration: data.duration || 0,
  194. price: data.price || 0,
  195. rating: data.rating || 4.5,
  196. isRecommended: data.isRecommended !== undefined ? data.isRecommended : (data.is_recommended !== undefined ? data.is_recommended : 0),
  197. coverImage: data.coverImage || data.cover_image || data.cover || '',
  198. description: data.description || '',
  199. content: data.content || '',
  200. status: data.status !== undefined ? data.status : 1
  201. })
  202. }
  203. } catch (error) {
  204. console.error('加载详情失败:', error)
  205. }
  206. }
  207. const handleSubmit = async () => {
  208. if (!formRef.value) return
  209. try {
  210. // 表单验证
  211. await formRef.value.validate()
  212. submitLoading.value = true
  213. const url = isEdit.value ? `${API_ENDPOINTS.COURSE_UPDATE}/${form.id}` : API_ENDPOINTS.COURSE_CREATE
  214. const method = isEdit.value ? 'put' : 'post'
  215. // 将前端的驼峰字段转换为后端期望的 snake_case 格式
  216. const submitData = {
  217. id: form.id,
  218. name: form.name,
  219. category_name: form.categoryName, // 转换为下划线格式
  220. instructor: form.instructor,
  221. teacher_name: form.instructor, // 兼容后端字段
  222. duration: form.duration,
  223. price: form.price,
  224. rating: form.rating,
  225. is_recommended: form.isRecommended,
  226. cover_image: form.coverImage,
  227. description: form.description,
  228. content: form.content,
  229. status: form.status
  230. }
  231. const response = await request[method](url, submitData)
  232. if (response.code === 200) {
  233. ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
  234. router.push('/course/list')
  235. } else {
  236. ElMessage.error(response.message || (isEdit.value ? '更新失败' : '创建失败'))
  237. }
  238. } catch (error) {
  239. // 判断是表单验证错误还是请求错误
  240. if (error && typeof error === 'object' && !error.response) {
  241. // 表单验证错误
  242. console.log('表单验证失败:', error)
  243. const firstError = Object.values(error)[0]
  244. if (firstError && firstError[0] && firstError[0].message) {
  245. ElMessage.warning(firstError[0].message)
  246. } else {
  247. ElMessage.warning('请检查表单必填项')
  248. }
  249. } else {
  250. // 请求错误
  251. console.error('提交失败:', error)
  252. ElMessage.error(error.response?.data?.message || (isEdit.value ? '更新失败' : '创建失败'))
  253. }
  254. } finally {
  255. submitLoading.value = false
  256. }
  257. }
  258. onMounted(() => {
  259. if (route.params.id) {
  260. isEdit.value = true
  261. loadDetail(route.params.id)
  262. }
  263. })
  264. </script>
  265. <style scoped>
  266. .course-form-container { padding: 0; }
  267. .page-title { font-size: 24px; font-weight: bold; color: #333; margin: 0 0 20px 0; }
  268. .cover-upload-wrapper {
  269. display: flex;
  270. align-items: flex-start;
  271. gap: 20px;
  272. }
  273. .cover-uploader {
  274. border: 1px dashed #d9d9d9;
  275. border-radius: 6px;
  276. cursor: pointer;
  277. position: relative;
  278. overflow: hidden;
  279. transition: all 0.3s;
  280. }
  281. .cover-uploader:hover {
  282. border-color: #409eff;
  283. }
  284. .cover-uploader-icon {
  285. font-size: 28px;
  286. color: #8c939d;
  287. width: 178px;
  288. height: 100px;
  289. display: flex;
  290. align-items: center;
  291. justify-content: center;
  292. background-color: #fbfdff;
  293. }
  294. .cover-preview {
  295. width: 178px;
  296. height: 100px;
  297. display: block;
  298. object-fit: cover;
  299. }
  300. .cover-tips {
  301. flex: 1;
  302. display: flex;
  303. flex-direction: column;
  304. gap: 10px;
  305. }
  306. .cover-tips p {
  307. margin: 0;
  308. color: #909399;
  309. font-size: 12px;
  310. line-height: 1.5;
  311. }
  312. </style>