528 lines
15 KiB
Vue
528 lines
15 KiB
Vue
<template>
|
||
<div class="course-edit-view">
|
||
<a-page-header
|
||
:title="isEdit ? '编辑课程包' : '创建课程包'"
|
||
@back="() => router.back()"
|
||
>
|
||
<template #extra>
|
||
<a-space>
|
||
<a-button @click="handleSaveDraft" :loading="saving">保存草稿</a-button>
|
||
<a-button v-if="isEdit" type="primary" @click="handleSaveAndSubmit" :loading="saving">
|
||
保存
|
||
</a-button>
|
||
</a-space>
|
||
</template>
|
||
</a-page-header>
|
||
|
||
<a-spin :spinning="loading" tip="正在加载课程数据...">
|
||
<a-card :bordered="false" style="margin-top: 16px;">
|
||
<!-- 步骤导航 -->
|
||
<a-steps :current="currentStep" size="small" @change="onStepChange">
|
||
<a-step title="基本信息" />
|
||
<a-step title="课程介绍" />
|
||
<a-step title="排课参考" />
|
||
<a-step title="导入课" />
|
||
<a-step title="集体课" />
|
||
<a-step title="领域课" />
|
||
<a-step title="环创建设" />
|
||
</a-steps>
|
||
|
||
<!-- 完成度进度条 -->
|
||
<div class="completion-bar">
|
||
<span>完成度</span>
|
||
<a-progress :percent="completionPercent" :status="completionStatus" size="small" />
|
||
</div>
|
||
|
||
<!-- 步骤内容 -->
|
||
<div class="step-content">
|
||
<!-- 步骤1:基本信息 -->
|
||
<Step1BasicInfo
|
||
v-show="currentStep === 0"
|
||
ref="step1Ref"
|
||
v-model="formData.basic"
|
||
@change="handleDataChange"
|
||
/>
|
||
|
||
<!-- 步骤2:课程介绍 -->
|
||
<Step2CourseIntro
|
||
v-show="currentStep === 1"
|
||
ref="step2Ref"
|
||
v-model="formData.intro"
|
||
@change="handleDataChange"
|
||
/>
|
||
|
||
<!-- 步骤3:排课参考 -->
|
||
<Step3ScheduleRef
|
||
v-show="currentStep === 2"
|
||
ref="step3Ref"
|
||
v-model="formData.scheduleRefData"
|
||
@change="handleDataChange"
|
||
/>
|
||
|
||
<!-- 步骤4:导入课 -->
|
||
<Step4IntroLesson
|
||
v-show="currentStep === 3"
|
||
ref="step4Ref"
|
||
:course-id="courseId"
|
||
@change="handleDataChange"
|
||
/>
|
||
|
||
<!-- 步骤5:集体课 -->
|
||
<Step5CollectiveLesson
|
||
v-show="currentStep === 4"
|
||
ref="step5Ref"
|
||
:course-id="courseId"
|
||
:course-name="formData.basic.name"
|
||
@change="handleDataChange"
|
||
/>
|
||
|
||
<!-- 步骤6:领域课 -->
|
||
<Step6DomainLessons
|
||
v-show="currentStep === 5"
|
||
ref="step6Ref"
|
||
:course-id="courseId"
|
||
:course-name="formData.basic.name"
|
||
@change="handleDataChange"
|
||
/>
|
||
|
||
<!-- 步骤7:环创建设 -->
|
||
<Step7Environment
|
||
v-show="currentStep === 6"
|
||
ref="step7Ref"
|
||
v-model="formData.environmentConstruction"
|
||
@change="handleDataChange"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 步骤导航按钮 -->
|
||
<div class="step-actions">
|
||
<a-button v-if="currentStep > 0" @click="prevStep">
|
||
上一步
|
||
</a-button>
|
||
<a-button v-if="currentStep < 6" type="primary" @click="nextStep">
|
||
下一步
|
||
</a-button>
|
||
<a-button v-if="currentStep === 6" type="primary" :loading="saving" @click="handleSave">
|
||
{{ isEdit ? '保存' : '创建' }}
|
||
</a-button>
|
||
</div>
|
||
</a-card>
|
||
</a-spin>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, provide } from 'vue';
|
||
import { useRouter, useRoute } from 'vue-router';
|
||
import { message } from 'ant-design-vue';
|
||
import Step1BasicInfo from './components/Step1BasicInfo.vue';
|
||
import Step2CourseIntro from './components/Step2CourseIntro.vue';
|
||
import Step3ScheduleRef from './components/Step3ScheduleRef.vue';
|
||
import Step4IntroLesson from './components/Step4IntroLesson.vue';
|
||
import Step5CollectiveLesson from './components/Step5CollectiveLesson.vue';
|
||
import Step6DomainLessons from './components/Step6DomainLessons.vue';
|
||
import Step7Environment from './components/Step7Environment.vue';
|
||
import { getCourse, createCourse, updateCourse } from '@/api/course';
|
||
import {
|
||
getLessonList,
|
||
createLesson,
|
||
updateLesson,
|
||
deleteLesson,
|
||
createStep,
|
||
updateStep,
|
||
deleteStep,
|
||
} from '@/api/lesson';
|
||
|
||
const router = useRouter();
|
||
const route = useRoute();
|
||
|
||
const isEdit = computed(() => !!route.params.id);
|
||
const courseId = computed(() => Number(route.params.id));
|
||
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const currentStep = ref(0);
|
||
|
||
// 步骤组件引用
|
||
const step1Ref = ref();
|
||
const step2Ref = ref();
|
||
const step3Ref = ref();
|
||
const step4Ref = ref();
|
||
const step5Ref = ref();
|
||
const step6Ref = ref();
|
||
const step7Ref = ref();
|
||
|
||
// 表单数据
|
||
const formData = reactive({
|
||
basic: {
|
||
name: '',
|
||
themeId: undefined as number | undefined,
|
||
grades: [] as string[],
|
||
pictureBookName: '',
|
||
coreContent: '',
|
||
duration: 25,
|
||
domainTags: [] as string[],
|
||
coverImagePath: '',
|
||
},
|
||
intro: {
|
||
introSummary: '',
|
||
introHighlights: '',
|
||
introGoals: '',
|
||
introSchedule: '',
|
||
introKeyPoints: '',
|
||
introMethods: '',
|
||
introEvaluation: '',
|
||
introNotes: '',
|
||
},
|
||
scheduleRefData: '',
|
||
environmentConstruction: '',
|
||
});
|
||
|
||
// 计算完成度
|
||
const completionPercent = computed(() => {
|
||
let filled = 0;
|
||
let total = 0;
|
||
|
||
// 基本信息必填项
|
||
total += 4;
|
||
if (formData.basic.name) filled++;
|
||
if (formData.basic.themeId) filled++;
|
||
if (formData.basic.grades.length > 0) filled++;
|
||
if (formData.basic.coreContent) filled++;
|
||
|
||
// 课程介绍(8项可选)
|
||
total += 8;
|
||
if (formData.intro.introSummary) filled++;
|
||
if (formData.intro.introHighlights) filled++;
|
||
if (formData.intro.introGoals) filled++;
|
||
if (formData.intro.introSchedule) filled++;
|
||
if (formData.intro.introKeyPoints) filled++;
|
||
if (formData.intro.introMethods) filled++;
|
||
if (formData.intro.introEvaluation) filled++;
|
||
if (formData.intro.introNotes) filled++;
|
||
|
||
return Math.round((filled / total) * 100);
|
||
});
|
||
|
||
const completionStatus = computed(() => {
|
||
if (completionPercent.value >= 75) return 'success';
|
||
if (completionPercent.value >= 50) return 'normal';
|
||
return 'exception';
|
||
});
|
||
|
||
// 获取课程详情
|
||
const fetchCourseDetail = async () => {
|
||
if (!isEdit.value) return;
|
||
|
||
loading.value = true;
|
||
try {
|
||
const res = await getCourse(courseId.value) as any;
|
||
const course = res.data || res;
|
||
|
||
// 基本信息
|
||
formData.basic.name = course.name;
|
||
formData.basic.themeId = course.themeId;
|
||
formData.basic.grades = course.gradeTags ? JSON.parse(course.gradeTags) : [];
|
||
formData.basic.pictureBookName = course.pictureBookName || '';
|
||
formData.basic.coreContent = course.coreContent || '';
|
||
formData.basic.duration = course.duration || 25;
|
||
formData.basic.domainTags = course.domainTags ? JSON.parse(course.domainTags) : [];
|
||
formData.basic.coverImagePath = course.coverImagePath || '';
|
||
|
||
// 课程介绍
|
||
formData.intro.introSummary = course.introSummary || '';
|
||
formData.intro.introHighlights = course.introHighlights || '';
|
||
formData.intro.introGoals = course.introGoals || '';
|
||
formData.intro.introSchedule = course.introSchedule || '';
|
||
formData.intro.introKeyPoints = course.introKeyPoints || '';
|
||
formData.intro.introMethods = course.introMethods || '';
|
||
formData.intro.introEvaluation = course.introEvaluation || '';
|
||
formData.intro.introNotes = course.introNotes || '';
|
||
|
||
// 排课参考
|
||
formData.scheduleRefData = course.scheduleRefData || '';
|
||
|
||
// 环创建设
|
||
formData.environmentConstruction = course.environmentConstruction || '';
|
||
} catch (error) {
|
||
message.error('获取课程详情失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
// 步骤导航
|
||
const onStepChange = (step: number) => {
|
||
if (step > currentStep.value) {
|
||
// 前进时验证当前步骤
|
||
if (!validateCurrentStep()) {
|
||
return;
|
||
}
|
||
}
|
||
currentStep.value = step;
|
||
};
|
||
|
||
const prevStep = () => {
|
||
if (currentStep.value > 0) {
|
||
currentStep.value--;
|
||
}
|
||
};
|
||
|
||
const nextStep = () => {
|
||
if (!validateCurrentStep()) {
|
||
return;
|
||
}
|
||
if (currentStep.value < 6) {
|
||
currentStep.value++;
|
||
}
|
||
};
|
||
|
||
// 验证当前步骤
|
||
const validateCurrentStep = () => {
|
||
if (currentStep.value === 0) {
|
||
const result = step1Ref.value?.validate();
|
||
if (!result?.valid) {
|
||
message.warning(result?.errors[0] || '请完成基本信息');
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// 处理数据变化
|
||
const handleDataChange = () => {
|
||
// 数据变化时可以自动保存草稿
|
||
};
|
||
|
||
// 保存草稿
|
||
const handleSaveDraft = async () => {
|
||
await handleSave(true);
|
||
};
|
||
|
||
// 保存
|
||
const handleSave = async (isDraft = false) => {
|
||
// 防止重复提交
|
||
if (saving.value) {
|
||
return;
|
||
}
|
||
|
||
saving.value = true;
|
||
let savedCourseId = courseId.value;
|
||
|
||
try {
|
||
// 1. 保存课程包基本信息
|
||
const courseData = {
|
||
name: formData.basic.name,
|
||
themeId: formData.basic.themeId,
|
||
gradeTags: JSON.stringify(formData.basic.grades),
|
||
pictureBookName: formData.basic.pictureBookName,
|
||
coreContent: formData.basic.coreContent,
|
||
duration: formData.basic.duration,
|
||
domainTags: JSON.stringify(formData.basic.domainTags),
|
||
coverImagePath: formData.basic.coverImagePath,
|
||
// 课程介绍
|
||
introSummary: formData.intro.introSummary,
|
||
introHighlights: formData.intro.introHighlights,
|
||
introGoals: formData.intro.introGoals,
|
||
introSchedule: formData.intro.introSchedule,
|
||
introKeyPoints: formData.intro.introKeyPoints,
|
||
introMethods: formData.intro.introMethods,
|
||
introEvaluation: formData.intro.introEvaluation,
|
||
introNotes: formData.intro.introNotes,
|
||
// 排课参考
|
||
scheduleRefData: formData.scheduleRefData,
|
||
// 环创建设
|
||
environmentConstruction: formData.environmentConstruction,
|
||
};
|
||
|
||
console.log('Saving course data...', { isDraft, isEdit: isEdit.value });
|
||
|
||
if (isEdit.value) {
|
||
await updateCourse(courseId.value, courseData);
|
||
console.log('Course updated successfully');
|
||
} else {
|
||
const res = await createCourse(courseData) as any;
|
||
savedCourseId = res.data?.id || res.id;
|
||
console.log('Course created with ID:', savedCourseId);
|
||
// 更新路由以支持后续保存
|
||
if (savedCourseId) {
|
||
router.replace(`/admin/courses/${savedCourseId}/edit`);
|
||
}
|
||
}
|
||
|
||
if (!savedCourseId) {
|
||
throw new Error('无法获取课程ID');
|
||
}
|
||
|
||
// 2. 保存导入课
|
||
try {
|
||
const introLessonData = step4Ref.value?.getSaveData();
|
||
if (introLessonData) {
|
||
console.log('Saving intro lesson...');
|
||
await saveLesson(savedCourseId, introLessonData, 'INTRO');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Failed to save intro lesson:', error);
|
||
// 继续保存其他内容,不中断
|
||
}
|
||
|
||
// 3. 保存集体课
|
||
try {
|
||
const collectiveLessonData = step5Ref.value?.getSaveData();
|
||
if (collectiveLessonData) {
|
||
console.log('Saving collective lesson...');
|
||
await saveLesson(savedCourseId, collectiveLessonData, 'COLLECTIVE');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Failed to save collective lesson:', error);
|
||
// 继续保存其他内容,不中断
|
||
}
|
||
|
||
// 4. 保存领域课
|
||
try {
|
||
const domainLessonsData = step6Ref.value?.getSaveData() || [];
|
||
for (const lessonData of domainLessonsData) {
|
||
console.log('Saving domain lesson:', lessonData.lessonType);
|
||
await saveLesson(savedCourseId, lessonData, lessonData.lessonType);
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Failed to save domain lessons:', error);
|
||
// 继续保存其他内容,不中断
|
||
}
|
||
|
||
message.success(isDraft ? '草稿保存成功' : (isEdit.value ? '保存成功' : '创建成功'));
|
||
|
||
if (!isDraft) {
|
||
router.push('/admin/courses');
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Save failed:', error);
|
||
const errorMsg = error.response?.data?.message || error.message || '未知错误';
|
||
const errorDetails = error.response?.data?.errors?.join(', ') || '';
|
||
message.error(`保存失败: ${errorMsg}${errorDetails ? ' - ' + errorDetails : ''}`);
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
};
|
||
|
||
// 保存单个课程
|
||
const saveLesson = async (courseId: number, lessonData: any, lessonType: string) => {
|
||
if (!lessonData) {
|
||
console.log('No lesson data to save for type:', lessonType);
|
||
return;
|
||
}
|
||
|
||
const lessonPayload = {
|
||
lessonType: lessonData.lessonType || lessonType,
|
||
name: lessonData.name || `${lessonType === 'INTRO' ? '导入课' : lessonType === 'COLLECTIVE' ? '集体课' : lessonType}`,
|
||
description: lessonData.description || '',
|
||
duration: lessonData.duration || 25,
|
||
videoPath: lessonData.videoPath || '',
|
||
videoName: lessonData.videoName || '',
|
||
pptPath: lessonData.pptPath || '',
|
||
pptName: lessonData.pptName || '',
|
||
pdfPath: lessonData.pdfPath || '',
|
||
pdfName: lessonData.pdfName || '',
|
||
objectives: lessonData.objectives || '',
|
||
preparation: lessonData.preparation || '',
|
||
extension: lessonData.extension || '',
|
||
reflection: lessonData.reflection || '',
|
||
assessmentData: lessonData.assessmentData || '',
|
||
useTemplate: lessonData.useTemplate || false,
|
||
};
|
||
|
||
let lessonId = lessonData.id;
|
||
|
||
try {
|
||
if (lessonData.isNew || !lessonData.id) {
|
||
// 创建新课程
|
||
console.log('Creating new lesson:', lessonType);
|
||
const res = await createLesson(courseId, lessonPayload) as any;
|
||
lessonId = res.data?.id || res.id;
|
||
console.log('Lesson created with ID:', lessonId);
|
||
} else {
|
||
// 更新现有课程
|
||
console.log('Updating lesson:', lessonId);
|
||
await updateLesson(lessonData.id, lessonPayload);
|
||
}
|
||
|
||
// 保存教学环节
|
||
if (lessonData.steps && lessonData.steps.length > 0 && lessonId) {
|
||
for (const step of lessonData.steps) {
|
||
const stepPayload = {
|
||
name: step.name || '教学环节',
|
||
content: step.content || '',
|
||
duration: step.duration || 5,
|
||
objective: step.objective || '',
|
||
};
|
||
|
||
try {
|
||
if (step.isNew || !step.id) {
|
||
await createStep(courseId, lessonId, stepPayload);
|
||
} else {
|
||
await updateStep(step.id, stepPayload);
|
||
}
|
||
} catch (stepError: any) {
|
||
console.error('Failed to save step:', stepError);
|
||
}
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
console.error('Failed to save lesson:', error);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
// 保存并提交
|
||
const handleSaveAndSubmit = async () => {
|
||
await handleSave(false);
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchCourseDetail();
|
||
});
|
||
|
||
// 提供courseId给子组件
|
||
provide('courseId', courseId);
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.course-edit-view {
|
||
padding: 24px;
|
||
}
|
||
|
||
.completion-bar {
|
||
margin-top: 16px;
|
||
padding: 12px 16px;
|
||
background: #fafafa;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
span {
|
||
font-size: 13px;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
:deep(.ant-progress) {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.step-content {
|
||
min-height: 400px;
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.step-actions {
|
||
margin-top: 32px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #f0f0f0;
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
}
|
||
</style>
|