fix(admin): 课程编辑加载与表单告警;FileUploader 预览 watch 顺序;教学环节 Form.ItemRest

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-24 18:05:34 +08:00
parent 94ea219f2f
commit c1ee18ca97
5 changed files with 44 additions and 55 deletions

View File

@ -2,7 +2,6 @@
<div class="step2-course-intro"> <div class="step2-course-intro">
<div class="intro-header"> <div class="intro-header">
<span class="title">课程介绍</span> <span class="title">课程介绍</span>
<a-tag color="blue">已填写 {{ filledCount }} / 8</a-tag>
</div> </div>
<a-tabs v-model:activeKey="activeTab" type="card"> <a-tabs v-model:activeKey="activeTab" type="card">
@ -176,20 +175,6 @@ watch(
// tab // tab
const currentTabIndex = computed(() => tabKeys.indexOf(activeTab.value)); const currentTabIndex = computed(() => tabKeys.indexOf(activeTab.value));
//
const filledCount = computed(() => {
let count = 0;
if (formData.introSummary) count++;
if (formData.introHighlights) count++;
if (formData.introGoals) count++;
if (formData.introSchedule) count++;
if (formData.introKeyPoints) count++;
if (formData.introMethods) count++;
if (formData.introEvaluation) count++;
if (formData.introNotes) count++;
return count;
});
// tab // tab
const prevTab = () => { const prevTab = () => {
const index = currentTabIndex.value; const index = currentTabIndex.value;
@ -221,7 +206,6 @@ const validate = () => {
defineExpose({ defineExpose({
validate, validate,
formData, formData,
filledCount,
}); });
</script> </script>

View File

@ -120,15 +120,6 @@ const imagePreviewLoading = ref(false);
const imagePreviewLoaded = ref(false); const imagePreviewLoaded = ref(false);
const imagePreviewError = ref(false); const imagePreviewError = ref(false);
// previewUrl
watch(() => previewUrl.value, (newUrl) => {
if (newUrl && props.fileType === 'image') {
imagePreviewLoading.value = true;
imagePreviewLoaded.value = false;
imagePreviewError.value = false;
}
}, { immediate: true });
const buttonText = computed(() => { const buttonText = computed(() => {
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
video: '上传视频', video: '上传视频',
@ -181,6 +172,19 @@ const previewUrl = computed(() => {
return `/uploads/${props.filePath}`; return `/uploads/${props.filePath}`;
}); });
// previewUrl watch previewUrl TDZCannot access before initialization
watch(
() => previewUrl.value,
(newUrl) => {
if (newUrl && props.fileType === 'image') {
imagePreviewLoading.value = true;
imagePreviewLoaded.value = false;
imagePreviewError.value = false;
}
},
{ immediate: true }
);
// PDF ppt PDF // PDF ppt PDF
const isPdfFile = computed(() => { const isPdfFile = computed(() => {
if (props.fileType === 'ppt' && props.filePath) { if (props.fileType === 'ppt' && props.filePath) {

View File

@ -25,7 +25,9 @@
style="width: 100%" style="width: 100%"
@change="handleChange" @change="handleChange"
/> />
<span class="duration-unit">分钟</span> <a-form-item-rest>
<span class="duration-unit">分钟</span>
</a-form-item-rest>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -137,11 +139,13 @@
</template> </template>
<div class="field-hint">至少添加一个环节每个环节需填写名称内容目标</div> <div class="field-hint">至少添加一个环节每个环节需填写名称内容目标</div>
<a-form-item name="steps"> <a-form-item name="steps">
<LessonStepsEditor <a-form-item-rest>
v-model="lessonData.steps" <LessonStepsEditor
:show-template="showTemplate" v-model="lessonData.steps"
@change="handleChange" :show-template="showTemplate"
/> @change="handleChange"
/>
</a-form-item-rest>
</a-form-item> </a-form-item>
</a-card> </a-card>

View File

@ -18,8 +18,11 @@
</a-page-header> </a-page-header>
</div> </div>
<a-spin :spinning="loading" tip="正在加载课程数据..."> <!-- 编辑态数据未就绪时单独展示加载避免 a-spin 包裹整表在 spinning 切换时与大量子组件补丁竞态导致 __vnode 报错 -->
<a-card :bordered="false" style="margin-top: 16px;"> <div v-if="!pageContentReady" class="course-edit-page-loading">
<a-spin size="large" tip="正在加载课程数据..." />
</div>
<a-card v-else :bordered="false" style="margin-top: 16px;">
<!-- 步骤导航 --> <!-- 步骤导航 -->
<a-steps :current="currentStep" size="small" @change="onStepChange"> <a-steps :current="currentStep" size="small" @change="onStepChange">
<a-step title="基本信息" /> <a-step title="基本信息" />
@ -62,12 +65,11 @@
@change="handleDataChange" /> @change="handleDataChange" />
</div> </div>
</a-card> </a-card>
</a-spin>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, computed, onMounted, provide } from 'vue'; import { ref, reactive, computed, onMounted, provide, nextTick } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import Step1BasicInfo from '@/components/course-edit/Step1BasicInfo.vue'; import Step1BasicInfo from '@/components/course-edit/Step1BasicInfo.vue';
@ -94,7 +96,8 @@ const route = useRoute();
const isEdit = computed(() => !!route.params.id); const isEdit = computed(() => !!route.params.id);
const courseId = computed(() => route.params.id as string | undefined); const courseId = computed(() => route.params.id as string | undefined);
const loading = ref(false); /** 编辑页:详情拉取完成后再挂载步骤表单,避免与 Spin 嵌套更新冲突 */
const pageContentReady = ref(!isEdit.value);
const saving = ref(false); const saving = ref(false);
const currentStep = ref(0); const currentStep = ref(0);
@ -137,7 +140,7 @@ const formData = reactive({
const fetchCourseDetail = async () => { const fetchCourseDetail = async () => {
if (!isEdit.value) return; if (!isEdit.value) return;
loading.value = true; pageContentReady.value = false;
try { try {
const res = await getCourse(courseId.value) as any; const res = await getCourse(courseId.value) as any;
const course = res.data || res; const course = res.data || res;
@ -180,10 +183,12 @@ const fetchCourseDetail = async () => {
// //
formData.environmentConstruction = course.environmentConstruction || ''; formData.environmentConstruction = course.environmentConstruction || '';
await nextTick();
pageContentReady.value = true;
} catch (error) { } catch (error) {
message.error('获取课程详情失败'); message.error('获取课程详情失败');
} finally { pageContentReady.value = true;
loading.value = false;
} }
}; };
@ -486,4 +491,12 @@ provide('courseId', courseId);
min-height: 400px; min-height: 400px;
margin-top: 24px; margin-top: 24px;
} }
.course-edit-page-loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 420px;
margin-top: 16px;
}
</style> </style>

View File

@ -2,7 +2,6 @@
<div class="step2-course-intro"> <div class="step2-course-intro">
<div class="intro-header"> <div class="intro-header">
<span class="title">课程介绍</span> <span class="title">课程介绍</span>
<a-tag color="blue">已填写 {{ filledCount }} / 8</a-tag>
</div> </div>
<a-tabs v-model:activeKey="activeTab" type="card"> <a-tabs v-model:activeKey="activeTab" type="card">
@ -176,20 +175,6 @@ watch(
// tab // tab
const currentTabIndex = computed(() => tabKeys.indexOf(activeTab.value)); const currentTabIndex = computed(() => tabKeys.indexOf(activeTab.value));
//
const filledCount = computed(() => {
let count = 0;
if (formData.introSummary) count++;
if (formData.introHighlights) count++;
if (formData.introGoals) count++;
if (formData.introSchedule) count++;
if (formData.introKeyPoints) count++;
if (formData.introMethods) count++;
if (formData.introEvaluation) count++;
if (formData.introNotes) count++;
return count;
});
// tab // tab
const prevTab = () => { const prevTab = () => {
const index = currentTabIndex.value; const index = currentTabIndex.value;
@ -221,7 +206,6 @@ const validate = () => {
defineExpose({ defineExpose({
validate, validate,
formData, formData,
filledCount,
}); });
</script> </script>