kindergarten_java/reading-platform-frontend/src/views/admin/courses/CourseEditView.vue

528 lines
15 KiB
Vue
Raw Normal View History

<template>
<div class="course-edit-view">
<a-page-header
:title="isEdit ? '编辑课程包' : '创建课程包'"
@back="() => router.back()"
2026-02-28 16:41:39 +08:00
>
<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>
2026-02-28 16:41:39 +08:00
</a-space>
</template>
</a-page-header>
2026-02-28 16:41:39 +08:00
<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>
2026-02-28 16:41:39 +08:00
<!-- 步骤内容 -->
<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>
2026-02-28 16:41:39 +08:00
<!-- 步骤导航按钮 -->
<div class="step-actions">
<a-button v-if="currentStep > 0" @click="prevStep">
上一步
</a-button>
2026-02-28 16:41:39 +08:00
<a-button v-if="currentStep < 6" type="primary" @click="nextStep">
下一步
</a-button>
2026-02-28 16:41:39 +08:00
<a-button v-if="currentStep === 6" type="primary" :loading="saving" @click="handleSave">
{{ isEdit ? '保存' : '创建' }}
</a-button>
2026-02-28 16:41:39 +08:00
</div>
</a-card>
</a-spin>
</div>
</template>
<script setup lang="ts">
2026-02-28 16:41:39 +08:00
import { ref, reactive, computed, onMounted, provide } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
2026-02-28 16:41:39 +08:00
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 {
2026-02-28 16:41:39 +08:00
getLessonList,
createLesson,
updateLesson,
deleteLesson,
createStep,
updateStep,
deleteStep,
} from '@/api/lesson';
const router = useRouter();
const route = useRoute();
2026-02-28 16:41:39 +08:00
const isEdit = computed(() => !!route.params.id);
const courseId = computed(() => Number(route.params.id));
const loading = ref(false);
2026-02-28 16:41:39 +08:00
const saving = ref(false);
const currentStep = ref(0);
2026-02-28 16:41:39 +08:00
// 步骤组件引用
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: '',
2026-02-28 16:41:39 +08:00
themeId: undefined as number | undefined,
grades: [] as string[],
pictureBookName: '',
2026-02-28 16:41:39 +08:00
coreContent: '',
duration: 25,
domainTags: [] as string[],
coverImagePath: '',
},
2026-02-28 16:41:39 +08:00
intro: {
introSummary: '',
introHighlights: '',
introGoals: '',
introSchedule: '',
introKeyPoints: '',
introMethods: '',
introEvaluation: '',
introNotes: '',
},
2026-02-28 16:41:39 +08:00
scheduleRefData: '',
environmentConstruction: '',
});
2026-02-28 16:41:39 +08:00
// 计算完成度
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);
});
2026-02-28 16:41:39 +08:00
const completionStatus = computed(() => {
if (completionPercent.value >= 75) return 'success';
if (completionPercent.value >= 50) return 'normal';
return 'exception';
});
2026-02-28 16:41:39 +08:00
// 获取课程详情
const fetchCourseDetail = async () => {
if (!isEdit.value) return;
2026-02-28 16:41:39 +08:00
loading.value = true;
try {
2026-02-28 16:41:39 +08:00
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;
}
};
2026-02-28 16:41:39 +08:00
// 步骤导航
const onStepChange = (step: number) => {
if (step > currentStep.value) {
// 前进时验证当前步骤
if (!validateCurrentStep()) {
return;
}
}
2026-02-28 16:41:39 +08:00
currentStep.value = step;
};
2026-02-28 16:41:39 +08:00
const prevStep = () => {
if (currentStep.value > 0) {
currentStep.value--;
}
};
2026-02-28 16:41:39 +08:00
const nextStep = () => {
if (!validateCurrentStep()) {
return;
}
2026-02-28 16:41:39 +08:00
if (currentStep.value < 6) {
currentStep.value++;
}
};
2026-02-28 16:41:39 +08:00
// 验证当前步骤
const validateCurrentStep = () => {
if (currentStep.value === 0) {
const result = step1Ref.value?.validate();
if (!result?.valid) {
message.warning(result?.errors[0] || '请完成基本信息');
return false;
}
}
2026-02-28 16:41:39 +08:00
return true;
};
2026-02-28 16:41:39 +08:00
// 处理数据变化
const handleDataChange = () => {
// 数据变化时可以自动保存草稿
};
2026-02-28 16:41:39 +08:00
// 保存草稿
const handleSaveDraft = async () => {
await handleSave(true);
};
2026-02-28 16:41:39 +08:00
// 保存
const handleSave = async (isDraft = false) => {
// 防止重复提交
if (saving.value) {
return;
}
saving.value = true;
2026-02-28 16:41:39 +08:00
let savedCourseId = courseId.value;
try {
2026-02-28 16:41:39 +08:00
// 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,
};
2026-02-28 16:41:39 +08:00
console.log('Saving course data...', { isDraft, isEdit: isEdit.value });
2026-02-28 16:41:39 +08:00
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`);
}
}
2026-02-28 16:41:39 +08:00
if (!savedCourseId) {
throw new Error('无法获取课程ID');
}
2026-02-28 16:41:39 +08:00
// 2. 保存导入课
try {
const introLessonData = step4Ref.value?.getSaveData();
if (introLessonData) {
console.log('Saving intro lesson...');
await saveLesson(savedCourseId, introLessonData, 'INTRO');
}
2026-02-28 16:41:39 +08:00
} catch (error: any) {
console.error('Failed to save intro lesson:', error);
// 继续保存其他内容,不中断
}
2026-02-28 16:41:39 +08:00
// 3. 保存集体课
try {
const collectiveLessonData = step5Ref.value?.getSaveData();
if (collectiveLessonData) {
console.log('Saving collective lesson...');
await saveLesson(savedCourseId, collectiveLessonData, 'COLLECTIVE');
}
2026-02-28 16:41:39 +08:00
} catch (error: any) {
console.error('Failed to save collective lesson:', error);
// 继续保存其他内容,不中断
}
2026-02-28 16:41:39 +08:00
// 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);
}
2026-02-28 16:41:39 +08:00
} catch (error: any) {
console.error('Failed to save domain lessons:', error);
// 继续保存其他内容,不中断
}
2026-02-28 16:41:39 +08:00
message.success(isDraft ? '草稿保存成功' : (isEdit.value ? '保存成功' : '创建成功'));
2026-02-28 16:41:39 +08:00
if (!isDraft) {
router.push('/admin/courses');
}
2026-02-28 16:41:39 +08:00
} 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;
}
2026-02-28 16:41:39 +08:00
};
2026-02-28 16:41:39 +08:00
// 保存单个课程
const saveLesson = async (courseId: number, lessonData: any, lessonType: string) => {
if (!lessonData) {
console.log('No lesson data to save for type:', lessonType);
return;
}
2026-02-28 16:41:39 +08:00
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,
};
2026-02-28 16:41:39 +08:00
let lessonId = lessonData.id;
2026-02-28 16:41:39 +08:00
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);
}
2026-02-28 16:41:39 +08:00
// 保存教学环节
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 || '',
};
2026-02-28 16:41:39 +08:00
try {
if (step.isNew || !step.id) {
await createStep(courseId, lessonId, stepPayload);
} else {
await updateStep(step.id, stepPayload);
}
2026-02-28 16:41:39 +08:00
} catch (stepError: any) {
console.error('Failed to save step:', stepError);
}
}
}
2026-02-28 16:41:39 +08:00
} catch (error: any) {
console.error('Failed to save lesson:', error);
throw error;
}
};
2026-02-28 16:41:39 +08:00
// 保存并提交
const handleSaveAndSubmit = async () => {
await handleSave(false);
};
2026-02-28 16:41:39 +08:00
onMounted(() => {
fetchCourseDetail();
});
2026-02-28 16:41:39 +08:00
// 提供courseId给子组件
provide('courseId', courseId);
</script>
2026-02-28 16:41:39 +08:00
<style scoped lang="scss">
.course-edit-view {
padding: 24px;
}
2026-02-28 16:41:39 +08:00
.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;
2026-02-28 16:41:39 +08:00
white-space: nowrap;
}
2026-02-28 16:41:39 +08:00
:deep(.ant-progress) {
flex: 1;
}
}
2026-02-28 16:41:39 +08:00
.step-content {
min-height: 400px;
margin-top: 24px;
}
2026-02-28 16:41:39 +08:00
.step-actions {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
display: flex;
gap: 12px;
justify-content: flex-end;
}
</style>