1045 lines
30 KiB
Vue
1045 lines
30 KiB
Vue
<template>
|
||
<div class="course-create-container">
|
||
<div class="form-container">
|
||
<!-- 表单内容 -->
|
||
<div class="form-content">
|
||
<div class="header-left">
|
||
<n-button quaternary circle size="large" @click="goBack">
|
||
<template #icon>
|
||
<n-icon>
|
||
<ArrowBackOutline />
|
||
</n-icon>
|
||
</template>
|
||
</n-button>
|
||
<h2 class="page-title">{{ pageTitle }}</h2>
|
||
</div>
|
||
|
||
<!-- 上半部分:两列布局 -->
|
||
<div class="form-row">
|
||
<!-- 左列 -->
|
||
<div class="form-column">
|
||
<!-- 课程名称 -->
|
||
<div class="form-item">
|
||
<label class="form-label required">课程名称:</label>
|
||
<n-input v-model:value="formData.courseName" placeholder="请输入课程名称" class="form-input" />
|
||
</div>
|
||
|
||
<!-- 课程开始时间 -->
|
||
<div class="form-item">
|
||
<label class="form-label required">课程开始时间:</label>
|
||
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="选择时间" class="form-input" />
|
||
</div>
|
||
|
||
<!-- 主讲老师 -->
|
||
<div class="form-item">
|
||
<label class="form-label required">主讲老师:</label>
|
||
<n-select v-model:value="formData.instructors" multiple :options="instructorOptions" placeholder="请选择主讲老师"
|
||
class="form-input" />
|
||
</div>
|
||
|
||
<!-- 参与学员 -->
|
||
<div class="form-item">
|
||
<label class="form-label required">参与学员:</label>
|
||
<div class="radio-container">
|
||
<label class="radio-option">
|
||
<input type="radio" name="studentType" value="all" v-model="formData.studentType"
|
||
class="radio-input" />
|
||
<span class="radio-label">全部学员</span>
|
||
</label>
|
||
<label class="radio-option">
|
||
<input type="radio" name="studentType" value="partial" v-model="formData.studentType"
|
||
class="radio-input" />
|
||
<span class="radio-label">仅部分学员</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 选择班级 -->
|
||
<div class="form-item" v-show="formData.studentType === 'partial'">
|
||
<label class="form-label required">选择班级:</label>
|
||
<n-select v-model:value="formData.selectedClasses" multiple :options="classOptions"
|
||
placeholder="选择班级(可多选)" class="form-input" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右列 -->
|
||
<div class="form-column">
|
||
<!-- 课程分类 -->
|
||
<div class="form-item">
|
||
<label class="form-label required">课程分类:</label>
|
||
<n-select v-model:value="formData.courseCategory" multiple :options="categoryOptions" placeholder="分类名称"
|
||
class="form-input" />
|
||
</div>
|
||
|
||
<!-- 排序 -->
|
||
<!-- <div class="form-item">
|
||
<label class="form-label required">排序:</label>
|
||
<n-input v-model:value="formData.sort" placeholder="请输入排序值" class="form-input" />
|
||
</div> -->
|
||
|
||
<!-- 课程结束时间 -->
|
||
<div class="form-item">
|
||
<label class="form-label required">课程结束时间:</label>
|
||
<n-date-picker v-model:value="formData.endTime" type="datetime" placeholder="选择时间" class="form-input" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间部分:课程封面 -->
|
||
<div class="form-section">
|
||
<div class="form-item">
|
||
<label class="form-label required">课程封面:</label>
|
||
<div class="cover-upload-container">
|
||
<div class="upload-area" @click="triggerFileUpload">
|
||
<template v-if="!previewUrl">
|
||
<div class="upload-icon">+</div>
|
||
<span class="upload-text">上传</span>
|
||
</template>
|
||
<div v-else class="preview-container">
|
||
<img :src="previewUrl" alt="课程封面预览" class="preview-image" />
|
||
<div class="delete-icon" @click.stop="clearUpload">×</div>
|
||
</div>
|
||
</div>
|
||
<input ref="fileInput" type="file" accept="image/*" @change="handleFileChange" style="display: none;" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 课程简介 -->
|
||
<div class="form-text">
|
||
<label class="form-label required">课程简介:</label>
|
||
<div class="rich-editor-container">
|
||
<div class="editor-container">
|
||
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig"
|
||
:mode="mode" />
|
||
<Editor style="height: 300px; overflow-y: hidden;" v-model="formData.courseDescription"
|
||
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下半部分:设置选项 -->
|
||
<div class="form-section">
|
||
<!-- 离开页面停止播放 -->
|
||
<div class="form-item btn-label">
|
||
<label class="form-label required">离开页面停止播放</label>
|
||
<div class="setting-container">
|
||
<n-switch v-model:value="formData.stopOnLeave" class="form-toggle" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视频倍数播放 -->
|
||
<div class="form-item btn-label">
|
||
<label class="form-label required">视频倍数播放</label>
|
||
<div class="setting-container">
|
||
<n-switch v-model:value="formData.videoSpeedControl" class="form-toggle" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 显示视频文本 -->
|
||
<div class="form-item btn-label">
|
||
<label class="form-label required">显示视频文本</label>
|
||
<div class="setting-container">
|
||
<n-switch v-model:value="formData.showVideoText" class="form-toggle" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 积分设置 -->
|
||
<div class="form-item form-integral" v-if="false">
|
||
<label class="form-label required">积分设置</label>
|
||
<div class="setting-container">
|
||
<n-switch v-model:value="formData.pointsEnabled" class="form-toggle" />
|
||
<span class="setting-label">学完可获得</span>
|
||
<n-input-number v-model:value="formData.earnPoints" :min="0" :max="1000" class="score-input"
|
||
:show-button="false" />
|
||
<span class="setting-label">积分</span>
|
||
<span class="setting-label">本课程需</span>
|
||
<n-input-number v-model:value="formData.requiredPoints" :min="0" :max="1000" class="score-input"
|
||
:show-button="false" />
|
||
<span class="setting-label">积分兑换</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部按钮 -->
|
||
<div class="form-footer">
|
||
<n-button class="cancel-btn" @click="handleCancel">取消</n-button>
|
||
<n-button type="primary" class="save-btn" @click="handleSubmit">保存</n-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { reactive, ref, shallowRef, onBeforeUnmount, onMounted, computed, Ref } from 'vue'
|
||
import { useRouter, useRoute } from 'vue-router'
|
||
import { useMessage } from 'naive-ui'
|
||
import { ArrowBackOutline } from '@vicons/ionicons5';
|
||
import {
|
||
NInput,
|
||
NDatePicker,
|
||
NSelect,
|
||
NSwitch,
|
||
NButton,
|
||
NInputNumber
|
||
} from 'naive-ui'
|
||
import '@wangeditor/editor/dist/css/style.css'
|
||
// @ts-ignore
|
||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||
import TeachCourseApi from '@/api/modules/teachCourse'
|
||
import UploadApi from '@/api/modules/upload'
|
||
import CourseApi from '@/api/modules/course';
|
||
import { useCourseStore } from '@/stores/course'
|
||
|
||
const router = useRouter()
|
||
const route = useRoute()
|
||
const message = useMessage()
|
||
const courseStore = useCourseStore()
|
||
|
||
// 判断是否为编辑模式
|
||
const isEditMode = computed(() => !!route.params.id)
|
||
const courseId = computed(() => route.params.id as string)
|
||
|
||
// 页面标题
|
||
const pageTitle = computed(() => isEditMode.value ? '编辑课程' : '创建课程')
|
||
|
||
// 编辑器实例,必须用 shallowRef
|
||
const editorRef = shallowRef()
|
||
|
||
// 文件上传相关
|
||
const fileInput = ref<HTMLInputElement>()
|
||
const previewUrl = ref('')
|
||
|
||
// 待设置的课程描述内容(用于富文本编辑器初始化后设置)
|
||
const pendingCourseDescription = ref('')
|
||
|
||
const toolbarConfig = {}
|
||
const editorConfig = { placeholder: '请输入内容...' }
|
||
const mode = 'default'
|
||
|
||
// 组件销毁时,也及时销毁编辑器
|
||
onBeforeUnmount(() => {
|
||
const editor = editorRef.value
|
||
if (editor == null) return
|
||
editor.destroy()
|
||
})
|
||
|
||
const handleCreated = (editor: any) => {
|
||
editorRef.value = editor // 记录 editor 实例,重要!
|
||
|
||
// 如果有待设置的内容,立即设置到编辑器
|
||
if (pendingCourseDescription.value) {
|
||
editor.setHtml(pendingCourseDescription.value);
|
||
formData.courseDescription = pendingCourseDescription.value;
|
||
pendingCourseDescription.value = ''; // 清空待设置内容
|
||
}
|
||
}
|
||
|
||
// 表单数据
|
||
const formData = reactive({
|
||
courseName: '',
|
||
courseCategory: [] as string[] | number[], // 改为 null 以显示 placeholder
|
||
instructors: [] as string[],
|
||
// sort: null as string | null, // 改为 null 以显示 placeholder
|
||
startTime: null as number | null,
|
||
endTime: null as number | null,
|
||
studentType: 'all' as 'all' | 'partial', // 'all' 或 'partial'
|
||
selectedClasses: [] as string[],
|
||
courseCover: null as File | null,
|
||
courseDescription: '',
|
||
// 视频设置
|
||
stopOnLeave: false,
|
||
videoSpeedControl: false,
|
||
showVideoText: false,
|
||
// 积分设置
|
||
pointsEnabled: true,
|
||
earnPoints: 60,
|
||
requiredPoints: 60
|
||
})
|
||
|
||
|
||
|
||
// 加载课程数据
|
||
const loadCourseData = async () => {
|
||
try {
|
||
// 首先检查store中是否有课程编辑数据
|
||
const storeCourseData = courseStore.courseEditData;
|
||
|
||
if (storeCourseData) {
|
||
// 字段映射:从后端数据映射到前端表单字段
|
||
formData.courseName = storeCourseData.name || storeCourseData.courseName || '';
|
||
// 处理课程分类:如果是字符串则分割为数组,如果是数组则直接使用
|
||
const categoryData = storeCourseData.categoryId || storeCourseData.courseCategory;
|
||
if (typeof categoryData === 'string' && categoryData) {
|
||
formData.courseCategory = categoryData.split(',').map(id => Number(id));
|
||
} else if (Array.isArray(categoryData)) {
|
||
formData.courseCategory = categoryData;
|
||
} else {
|
||
formData.courseCategory = [];
|
||
}
|
||
formData.instructors = storeCourseData.instructors || [];
|
||
formData.startTime = storeCourseData.start_time || storeCourseData.startTime || null;
|
||
formData.endTime = storeCourseData.end_time || storeCourseData.endTime || null;
|
||
formData.studentType = (storeCourseData.type === 1 || storeCourseData.studentType === 'partial') ? 'partial' : 'all';
|
||
formData.selectedClasses = storeCourseData.target ? storeCourseData.target.split(',') : (storeCourseData.selectedClasses || []);
|
||
|
||
const tempCourseDescription = storeCourseData.description || storeCourseData.courseDescription || '';
|
||
if (tempCourseDescription) {
|
||
if (editorRef.value) {
|
||
editorRef.value.setHtml(tempCourseDescription);
|
||
formData.courseDescription = tempCourseDescription;
|
||
} else {
|
||
pendingCourseDescription.value = tempCourseDescription;
|
||
}
|
||
}
|
||
|
||
// 视频设置选项:从后端字段映射到前端字段(数字0/1转布尔值)
|
||
formData.stopOnLeave = storeCourseData.pauseExit !== undefined ?
|
||
Boolean(Number(storeCourseData.pauseExit)) : true;
|
||
formData.videoSpeedControl = storeCourseData.allowSpeed !== undefined ?
|
||
Boolean(Number(storeCourseData.allowSpeed)) : false;
|
||
formData.showVideoText = storeCourseData.showSubtitle !== undefined ?
|
||
Boolean(Number(storeCourseData.showSubtitle)) : true;
|
||
|
||
// 积分设置
|
||
formData.pointsEnabled = storeCourseData.pointsEnabled !== undefined ? storeCourseData.pointsEnabled : true;
|
||
formData.earnPoints = storeCourseData.earnPoints || 60;
|
||
formData.requiredPoints = storeCourseData.requiredPoints || 60;
|
||
|
||
if (storeCourseData.cover || storeCourseData.courseCover) {
|
||
previewUrl.value = storeCourseData.cover || storeCourseData.courseCover;
|
||
formData.courseCover = null;
|
||
}
|
||
|
||
message.success('课程数据加载成功');
|
||
return; // 如果从store获取到数据,就不需要再调用API
|
||
}
|
||
|
||
// 如果store中没有数据,检查是否有通过路由传递的课程数据(向后兼容)
|
||
const routeCourseData = route.query.courseData;
|
||
if (routeCourseData) {
|
||
try {
|
||
const courseData = JSON.parse(routeCourseData as string);
|
||
|
||
// 字段映射:从后端数据映射到前端表单字段
|
||
formData.courseName = courseData.name || courseData.courseName || '';
|
||
// 处理课程分类:如果是字符串则分割为数组,如果是数组则直接使用
|
||
const categoryData = courseData.categoryId || courseData.courseCategory;
|
||
if (typeof categoryData === 'string' && categoryData) {
|
||
formData.courseCategory = categoryData.split(',').map(id => Number(id));
|
||
} else if (Array.isArray(categoryData)) {
|
||
formData.courseCategory = categoryData;
|
||
} else {
|
||
formData.courseCategory = [];
|
||
}
|
||
formData.instructors = courseData.instructors || [];
|
||
formData.startTime = courseData.start_time || courseData.startTime || null;
|
||
formData.endTime = courseData.end_time || courseData.endTime || null;
|
||
formData.studentType = (courseData.type === 1 || courseData.studentType === 'partial') ? 'partial' : 'all';
|
||
formData.selectedClasses = courseData.target ? courseData.target.split(',') : (courseData.selectedClasses || []);
|
||
|
||
// 处理富文本编辑器内容
|
||
const tempCourseDescription = courseData.description || courseData.courseDescription || '';
|
||
if (tempCourseDescription) {
|
||
if (editorRef.value) {
|
||
// 编辑器已经初始化,直接设置
|
||
editorRef.value.setHtml(tempCourseDescription);
|
||
formData.courseDescription = tempCourseDescription;
|
||
} else {
|
||
// 编辑器还未初始化,保存到待设置变量
|
||
pendingCourseDescription.value = tempCourseDescription;
|
||
}
|
||
}
|
||
|
||
// 视频设置选项:从后端字段映射到前端字段(数字0/1转布尔值)
|
||
formData.stopOnLeave = courseData.pauseExit !== undefined ?
|
||
Boolean(Number(courseData.pauseExit)) : true;
|
||
formData.videoSpeedControl = courseData.allowSpeed !== undefined ?
|
||
Boolean(Number(courseData.allowSpeed)) : false;
|
||
formData.showVideoText = courseData.showSubtitle !== undefined ?
|
||
Boolean(Number(courseData.showSubtitle)) : true;
|
||
|
||
// 积分设置(使用默认值或数据值)
|
||
formData.pointsEnabled = courseData.pointsEnabled !== undefined ? courseData.pointsEnabled : true;
|
||
formData.earnPoints = courseData.earnPoints || 60;
|
||
formData.requiredPoints = courseData.requiredPoints || 60;
|
||
|
||
// 如果有课程封面,设置预览URL
|
||
if (courseData.cover || courseData.courseCover) {
|
||
previewUrl.value = courseData.cover || courseData.courseCover;
|
||
formData.courseCover = null; // 现有URL,不是新文件
|
||
}
|
||
|
||
message.success('课程数据加载成功');
|
||
return; // 如果从路由获取到数据,就不需要再调用API
|
||
} catch (parseError) {
|
||
// 如果解析失败,继续使用原来的模拟数据逻辑
|
||
}
|
||
}
|
||
|
||
// 如果既没有store数据也没有路由数据,保持表单为空状态以供新建课程
|
||
} catch (error) {
|
||
message.error('加载课程数据失败')
|
||
}
|
||
}
|
||
|
||
// 组件挂载时处理
|
||
onMounted(() => {
|
||
// 如果是编辑模式(有courseId)、有通过路由传递的课程数据,或者store中有数据
|
||
if (isEditMode.value && courseId.value) {
|
||
loadCourseData()
|
||
} else if (route.query.courseData || courseStore.courseEditData) {
|
||
// 即使没有ID,也尝试从路由数据或store加载(用于从列表页面直接编辑的情况)
|
||
loadCourseData()
|
||
}
|
||
})
|
||
|
||
// 课程分类选项
|
||
const categoryOptions: Ref<{ label: string; value: number }[]> = ref([])
|
||
|
||
// 讲师选项
|
||
const instructorOptions = ref([] as { label: string; value: string }[])
|
||
|
||
// 班级选项
|
||
const classOptions = [
|
||
{ label: '前端开发班', value: 'frontend-class' },
|
||
{ label: '后端开发班', value: 'backend-class' },
|
||
{ label: 'AI算法班', value: 'ai-class' },
|
||
{ label: '全栈开发班', value: 'fullstack-class' }
|
||
]
|
||
|
||
// 文件上传相关方法
|
||
const triggerFileUpload = () => {
|
||
fileInput.value?.click()
|
||
}
|
||
|
||
const handleFileChange = (event: Event) => {
|
||
const input = event.target as HTMLInputElement
|
||
if (input.files && input.files.length > 0) {
|
||
const file = input.files[0]
|
||
|
||
// 验证文件类型
|
||
if (!file.type.startsWith('image/')) {
|
||
message.error('请上传图片文件')
|
||
return
|
||
}
|
||
|
||
// 验证文件大小(限制为2MB)
|
||
if (file.size > 2 * 1024 * 1024) {
|
||
message.error('图片大小不能超过2MB')
|
||
return
|
||
}
|
||
|
||
// 创建预览URL
|
||
previewUrl.value = URL.createObjectURL(file)
|
||
// 设置新的文件对象,这样在提交时就知道需要上传
|
||
formData.courseCover = file
|
||
}
|
||
}
|
||
|
||
const clearUpload = () => {
|
||
// 只有当previewUrl是本地创建的URL时才需要释放
|
||
if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
|
||
URL.revokeObjectURL(previewUrl.value)
|
||
}
|
||
|
||
// 清除预览和表单数据
|
||
previewUrl.value = ''
|
||
formData.courseCover = null
|
||
|
||
// 重置文件输入框
|
||
if (fileInput.value) {
|
||
fileInput.value.value = ''
|
||
}
|
||
}
|
||
|
||
// 取消
|
||
const handleCancel = () => {
|
||
// 取消时清除缓存数据
|
||
courseStore.clearCourseEditData();
|
||
router.back()
|
||
}
|
||
|
||
// 格式化时间为 YYYY-MM-DD HH:mm:ss 格式
|
||
const formatDateTime = (timestamp: number): string => {
|
||
const date = new Date(timestamp)
|
||
|
||
const year = date.getFullYear()
|
||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||
const day = date.getDate().toString().padStart(2, '0')
|
||
const hours = date.getHours().toString().padStart(2, '0')
|
||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||
const seconds = date.getSeconds().toString().padStart(2, '0')
|
||
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
}
|
||
|
||
// 保存
|
||
const handleSubmit = async () => {
|
||
try {
|
||
// 表单验证
|
||
if (!formData.courseName) {
|
||
message.error('请输入课程名称')
|
||
return
|
||
}
|
||
|
||
if (!formData.courseCategory || formData.courseCategory.length === 0) {
|
||
message.error('请选择课程分类')
|
||
return
|
||
}
|
||
|
||
if (formData.instructors.length === 0) {
|
||
message.error('请选择主讲老师')
|
||
return
|
||
}
|
||
|
||
if (!formData.startTime) {
|
||
message.error('请选择课程开始时间')
|
||
return
|
||
}
|
||
|
||
if (!formData.endTime) {
|
||
message.error('请选择课程结束时间')
|
||
return
|
||
}
|
||
|
||
if (!formData.courseCover && !previewUrl.value) {
|
||
message.error('请上传课程封面')
|
||
return
|
||
}
|
||
|
||
// 如果选择了仅部分学员,必须选择班级
|
||
if (formData.studentType === 'partial' && formData.selectedClasses.length === 0) {
|
||
message.error('请选择参与的班级')
|
||
return
|
||
}
|
||
|
||
message.loading('正在保存课程信息...')
|
||
|
||
let coverUrl = previewUrl.value
|
||
|
||
// 只有在有新的文件(File对象)时才需要上传
|
||
// 如果 courseCover 是 File 对象,说明用户选择了新文件,需要上传
|
||
// 如果 courseCover 是 null 但 previewUrl 有值,说明是编辑模式使用现有图片
|
||
|
||
if (formData.courseCover && formData.courseCover instanceof File) {
|
||
try {
|
||
const uploadResponse = await UploadApi.uploadCourseThumbnail(formData.courseCover)
|
||
|
||
if (uploadResponse.data.success) {
|
||
coverUrl = uploadResponse.data.message
|
||
} else {
|
||
message.error('课程封面上传失败')
|
||
return
|
||
}
|
||
} catch (uploadError) {
|
||
message.error('课程封面上传失败,请重试')
|
||
return
|
||
}
|
||
}
|
||
|
||
// 构建API请求参数
|
||
const createCourseData = {
|
||
name: formData.courseName,
|
||
cover: coverUrl,
|
||
description: formData.courseDescription,
|
||
type: formData.studentType === 'all' ? 0 : 1,
|
||
categoryId: Array.isArray(formData.courseCategory) ? formData.courseCategory.join(',') : formData.courseCategory,
|
||
target: formData.studentType === 'all' ? '' : formData.selectedClasses.join(','), // target 字段处理
|
||
start_time: formData.startTime ? formatDateTime(formData.startTime) : null,
|
||
end_time: formData.endTime ? formatDateTime(formData.endTime) : null,
|
||
pauseExit: formData.stopOnLeave ? '1' : '0', // 离开页面停止播放
|
||
allowSpeed: formData.videoSpeedControl ? '1' : '0', // 视频倍数播放
|
||
showSubtitle: formData.showVideoText ? '1' : '0', // 显示视频文本
|
||
// 其他可选字段
|
||
school: null,
|
||
video: null,
|
||
difficulty: null,
|
||
subject: null,
|
||
outline: null,
|
||
prerequisite: null,
|
||
reference: null,
|
||
arrangement: null,
|
||
enroll_count: null,
|
||
max_enroll: null,
|
||
status: 1, // 默认状态为进行中
|
||
question: null
|
||
}
|
||
|
||
if (isEditMode.value) {
|
||
// 编辑模式
|
||
const editData = {
|
||
id: courseId.value,
|
||
...createCourseData
|
||
}
|
||
const response = await TeachCourseApi.editCourse(editData)
|
||
|
||
if (response.data.code === 200) {
|
||
message.success('课程更新成功!')
|
||
// 清除缓存数据
|
||
courseStore.clearCourseEditData();
|
||
// 返回到课程管理页面
|
||
router.push('/teacher/course-management')
|
||
} else {
|
||
message.error(response.message || '更新失败,请重试')
|
||
}
|
||
} else {
|
||
// 创建模式
|
||
const response = await TeachCourseApi.createCourse(createCourseData)
|
||
|
||
if (response.data.code === 200) {
|
||
message.success('课程创建成功!')
|
||
// 清除缓存数据
|
||
courseStore.clearCourseEditData();
|
||
// 返回到课程管理页面
|
||
router.push('/teacher/course-management')
|
||
} else {
|
||
message.error(response.message || '创建失败,请重试')
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
const errorMessage = isEditMode.value ? '更新失败,请重试' : '创建失败,请重试'
|
||
message.error(errorMessage)
|
||
}
|
||
}
|
||
|
||
const goBack = () => {
|
||
// 返回时清除缓存数据
|
||
courseStore.clearCourseEditData();
|
||
router.back()
|
||
}
|
||
|
||
// 获取课程分类
|
||
const getCourseList = () => {
|
||
CourseApi.getCategories().then(response => {
|
||
categoryOptions.value = response.data.map((category: any) => ({
|
||
label: category.name,
|
||
value: category.id
|
||
}))
|
||
// 初始化时设置为空数组,而不是单个值
|
||
if (formData.courseCategory.length === 0 && categoryOptions.value.length > 0) {
|
||
formData.courseCategory = [];
|
||
}
|
||
}).catch(error => {
|
||
console.error('获取课程列表失败:', error)
|
||
})
|
||
}
|
||
|
||
// 获取老师
|
||
const getTeacherList = () => {
|
||
TeachCourseApi.getTeacherList().then(response => {
|
||
instructorOptions.value = response.data.result.map((teacher: any) => ({
|
||
label: teacher.realname,
|
||
value: teacher.id
|
||
}))
|
||
}).catch(error => {
|
||
console.error('获取老师列表失败:', error)
|
||
})
|
||
}
|
||
|
||
onMounted(() => {
|
||
getCourseList()
|
||
getTeacherList()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.course-create-container {
|
||
min-height: 100vh;
|
||
padding-bottom: 80px;
|
||
}
|
||
|
||
.form-container {
|
||
background: white;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.form-content {
|
||
padding: 24px 24px 0 24px;
|
||
}
|
||
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.page-title {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.form-row {
|
||
display: flex;
|
||
gap: 100px;
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.form-column {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-text {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-integral {
|
||
margin-top: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
min-width: 125px;
|
||
flex-shrink: 0;
|
||
text-align: right;
|
||
}
|
||
|
||
.form-label.required::before {
|
||
content: '*';
|
||
color: #ff4d4f;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.form-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* 最简单的输入框高度统一方法 */
|
||
.form-input {
|
||
height: 42px;
|
||
}
|
||
|
||
/* 强制设置下拉菜单高度 */
|
||
.form-input :deep(.n-base-selection) {
|
||
--n-height: 42px !important;
|
||
height: 42px !important;
|
||
}
|
||
|
||
.form-input :deep(.n-base-selection-label) {
|
||
height: 42px !important;
|
||
line-height: 42px !important;
|
||
}
|
||
|
||
/* 强制设置时间选择器高度 */
|
||
.form-input :deep(.n-input__input) {
|
||
height: 42px !important;
|
||
}
|
||
|
||
.form-input :deep(.n-input__input-el) {
|
||
height: 42px !important;
|
||
line-height: 42px !important;
|
||
}
|
||
|
||
/* 隐藏下拉菜单的加载状态和清除按钮 */
|
||
.form-input :deep(.n-base-loading),
|
||
.form-input :deep(.n-base-clear) {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 隐藏默认下拉箭头 */
|
||
.form-input :deep(.n-base-suffix__arrow) {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 为下拉菜单添加自定义箭头 */
|
||
.form-input :deep(.n-base-selection) {
|
||
position: relative;
|
||
}
|
||
|
||
.form-input :deep(.n-base-selection)::after {
|
||
content: '';
|
||
position: absolute;
|
||
right: 14px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 13px;
|
||
height: 8px;
|
||
background-image: url('/images/teacher/箭头-灰.png');
|
||
background-size: contain;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 自定义时间选择器图标 */
|
||
.form-input :deep(.n-date-picker-icon svg) {
|
||
display: none !important;
|
||
}
|
||
|
||
.form-input :deep(.n-date-picker-icon) {
|
||
background-image: url('/images/teacher/日历.png') !important;
|
||
background-size: contain !important;
|
||
background-repeat: no-repeat !important;
|
||
background-position: center !important;
|
||
width: 14px !important;
|
||
height: 14px !important;
|
||
}
|
||
|
||
/* 多选标签样式调整 */
|
||
.form-input :deep(.n-tag) {
|
||
background-color: #F5F8FB !important;
|
||
border: none !important;
|
||
color: #666666 !important;
|
||
margin-top: 3px;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.form-input :deep(.n-tag__content) {
|
||
color: #666666 !important;
|
||
}
|
||
|
||
.form-input :deep(.n-tag__border) {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 关闭按钮样式调整 */
|
||
.form-input :deep(.n-tag__close) {
|
||
width: 12px !important;
|
||
height: 12px !important;
|
||
}
|
||
|
||
.form-input :deep(.n-tag__close .n-base-icon) {
|
||
width: 12px !important;
|
||
height: 12px !important;
|
||
color: #999 !important;
|
||
}
|
||
|
||
.form-input :deep(.n-tag__close svg) {
|
||
width: 12px !important;
|
||
height: 12px !important;
|
||
}
|
||
|
||
.form-toggle {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.btn-label {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.toggle-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex: 1;
|
||
}
|
||
|
||
.toggle-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
/* 单选按钮样式 */
|
||
.radio-container {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 20px;
|
||
flex: 1;
|
||
}
|
||
|
||
.radio-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.radio-input {
|
||
width: 16px;
|
||
height: 16px;
|
||
accent-color: #0288D1;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.radio-label {
|
||
font-size: 14px;
|
||
color: #333;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 课程封面上传区域 */
|
||
.cover-upload-container {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.upload-area {
|
||
width: 160px;
|
||
height: 130px;
|
||
border: 2px dashed #d9d9d9;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: border-color 0.3s;
|
||
background-color: #fafafa;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #0288D1;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 32px;
|
||
color: #999;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.preview-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.preview-image {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.delete-icon {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
color: white;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.delete-icon:hover {
|
||
background-color: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.form-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.rich-editor-container {
|
||
flex: 1;
|
||
border: 1px solid #D8D8D8;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.setting-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex: 1;
|
||
}
|
||
|
||
.setting-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.score-input {
|
||
width: 80px;
|
||
}
|
||
|
||
.form-footer {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
padding: 20px 24px;
|
||
background: #fff;
|
||
border-top: 1px solid #e6e6e6;
|
||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||
z-index: 1000;
|
||
}
|
||
|
||
.cancel-btn {
|
||
min-width: 60px;
|
||
height: 32px;
|
||
border: 1px solid #0288D1;
|
||
background: #E2F5FF;
|
||
color: #0288D1;
|
||
}
|
||
|
||
.cancel-btn:hover {
|
||
border-color: #0277BD;
|
||
background: #B3E5FC;
|
||
color: #0277BD;
|
||
}
|
||
|
||
.save-btn {
|
||
min-width: 60px;
|
||
height: 32px;
|
||
background: #0288D1;
|
||
border-color: #0288D1;
|
||
color: white;
|
||
}
|
||
|
||
.save-btn:hover {
|
||
background: #0277BD;
|
||
border-color: #0277BD;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.form-row {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.form-content {
|
||
padding: 16px;
|
||
}
|
||
}
|
||
</style>
|