2026-02-26 15:22:26 +08:00
|
|
|
|
<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">
|
|
|
|
|
|
保存
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</a-button>
|
2026-02-28 16:41:39 +08:00
|
|
|
|
</a-space>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-page-header>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
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" />
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</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"
|
|
|
|
|
|
/>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
<!-- 步骤导航按钮 -->
|
|
|
|
|
|
<div class="step-actions">
|
|
|
|
|
|
<a-button v-if="currentStep > 0" @click="prevStep">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
上一步
|
|
|
|
|
|
</a-button>
|
2026-02-28 16:41:39 +08:00
|
|
|
|
<a-button v-if="currentStep < 6" type="primary" @click="nextStep">
|
2026-02-26 15:22:26 +08:00
|
|
|
|
下一步
|
|
|
|
|
|
</a-button>
|
2026-02-28 16:41:39 +08:00
|
|
|
|
<a-button v-if="currentStep === 6" type="primary" :loading="saving" @click="handleSave">
|
|
|
|
|
|
{{ isEdit ? '保存' : '创建' }}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</a-button>
|
2026-02-28 16:41:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-card>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-02-28 16:41:39 +08:00
|
|
|
|
import { ref, reactive, computed, onMounted, provide } from 'vue';
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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';
|
2026-02-26 15:22:26 +08:00
|
|
|
|
import {
|
2026-02-28 16:41:39 +08:00
|
|
|
|
getLessonList,
|
|
|
|
|
|
createLesson,
|
|
|
|
|
|
updateLesson,
|
|
|
|
|
|
deleteLesson,
|
|
|
|
|
|
createStep,
|
|
|
|
|
|
updateStep,
|
|
|
|
|
|
deleteStep,
|
|
|
|
|
|
} from '@/api/lesson';
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
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));
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
const loading = ref(false);
|
2026-02-28 16:41:39 +08:00
|
|
|
|
const saving = ref(false);
|
|
|
|
|
|
const currentStep = ref(0);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
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({
|
2026-02-26 15:22:26 +08:00
|
|
|
|
basic: {
|
|
|
|
|
|
name: '',
|
2026-02-28 16:41:39 +08:00
|
|
|
|
themeId: undefined as number | undefined,
|
|
|
|
|
|
grades: [] as string[],
|
2026-02-26 15:22:26 +08:00
|
|
|
|
pictureBookName: '',
|
2026-02-28 16:41:39 +08:00
|
|
|
|
coreContent: '',
|
|
|
|
|
|
duration: 25,
|
|
|
|
|
|
domainTags: [] as string[],
|
|
|
|
|
|
coverImagePath: '',
|
2026-02-26 15:22:26 +08:00
|
|
|
|
},
|
2026-02-28 16:41:39 +08:00
|
|
|
|
intro: {
|
|
|
|
|
|
introSummary: '',
|
|
|
|
|
|
introHighlights: '',
|
|
|
|
|
|
introGoals: '',
|
|
|
|
|
|
introSchedule: '',
|
|
|
|
|
|
introKeyPoints: '',
|
|
|
|
|
|
introMethods: '',
|
|
|
|
|
|
introEvaluation: '',
|
|
|
|
|
|
introNotes: '',
|
2026-02-26 15:22:26 +08:00
|
|
|
|
},
|
2026-02-28 16:41:39 +08:00
|
|
|
|
scheduleRefData: '',
|
|
|
|
|
|
environmentConstruction: '',
|
2026-02-26 15:22:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 获取课程详情
|
|
|
|
|
|
const fetchCourseDetail = async () => {
|
|
|
|
|
|
if (!isEdit.value) return;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
loading.value = true;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 步骤导航
|
|
|
|
|
|
const onStepChange = (step: number) => {
|
|
|
|
|
|
if (step > currentStep.value) {
|
|
|
|
|
|
// 前进时验证当前步骤
|
|
|
|
|
|
if (!validateCurrentStep()) {
|
|
|
|
|
|
return;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
currentStep.value = step;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
const prevStep = () => {
|
|
|
|
|
|
if (currentStep.value > 0) {
|
|
|
|
|
|
currentStep.value--;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
const nextStep = () => {
|
|
|
|
|
|
if (!validateCurrentStep()) {
|
2026-02-26 15:22:26 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
if (currentStep.value < 6) {
|
|
|
|
|
|
currentStep.value++;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
return true;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 处理数据变化
|
|
|
|
|
|
const handleDataChange = () => {
|
|
|
|
|
|
// 数据变化时可以自动保存草稿
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 保存草稿
|
|
|
|
|
|
const handleSaveDraft = async () => {
|
|
|
|
|
|
await handleSave(true);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 保存
|
|
|
|
|
|
const handleSave = async (isDraft = false) => {
|
|
|
|
|
|
// 防止重复提交
|
|
|
|
|
|
if (saving.value) {
|
|
|
|
|
|
return;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
saving.value = true;
|
2026-02-28 16:41:39 +08:00
|
|
|
|
let savedCourseId = courseId.value;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
console.log('Saving course data...', { isDraft, isEdit: isEdit.value });
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
if (!savedCourseId) {
|
|
|
|
|
|
throw new Error('无法获取课程ID');
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('Failed to save intro lesson:', error);
|
|
|
|
|
|
// 继续保存其他内容,不中断
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('Failed to save collective lesson:', error);
|
|
|
|
|
|
// 继续保存其他内容,不中断
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('Failed to save domain lessons:', error);
|
|
|
|
|
|
// 继续保存其他内容,不中断
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
message.success(isDraft ? '草稿保存成功' : (isEdit.value ? '保存成功' : '创建成功'));
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
if (!isDraft) {
|
|
|
|
|
|
router.push('/admin/courses');
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
};
|
2026-02-26 15:22:26 +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-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
let lessonId = lessonData.id;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
|
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-26 15:22:26 +08:00
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
} catch (stepError: any) {
|
|
|
|
|
|
console.error('Failed to save step:', stepError);
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-28 16:41:39 +08:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('Failed to save lesson:', error);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 保存并提交
|
|
|
|
|
|
const handleSaveAndSubmit = async () => {
|
|
|
|
|
|
await handleSave(false);
|
|
|
|
|
|
};
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchCourseDetail();
|
|
|
|
|
|
});
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
// 提供courseId给子组件
|
|
|
|
|
|
provide('courseId', courseId);
|
|
|
|
|
|
</script>
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.course-edit-view {
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
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;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
color: #666;
|
2026-02-28 16:41:39 +08:00
|
|
|
|
white-space: nowrap;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
:deep(.ant-progress) {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
2026-02-28 16:41:39 +08:00
|
|
|
|
.step-content {
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
}
|
2026-02-26 15:22:26 +08:00
|
|
|
|
|
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;
|
2026-02-26 15:22:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|