kindergarten_java/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue
2026-02-28 16:41:39 +08:00

528 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>