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="intro-header">
<span class="title">课程介绍</span>
<a-tag color="blue">已填写 {{ filledCount }} / 8</a-tag>
</div>
<a-tabs v-model:activeKey="activeTab" type="card">
@ -176,20 +175,6 @@ watch(
// tab
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
const prevTab = () => {
const index = currentTabIndex.value;
@ -221,7 +206,6 @@ const validate = () => {
defineExpose({
validate,
formData,
filledCount,
});
</script>

View File

@ -120,15 +120,6 @@ const imagePreviewLoading = ref(false);
const imagePreviewLoaded = 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 typeMap: Record<string, string> = {
video: '上传视频',
@ -181,6 +172,19 @@ const previewUrl = computed(() => {
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
const isPdfFile = computed(() => {
if (props.fileType === 'ppt' && props.filePath) {

View File

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

View File

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

View File

@ -2,7 +2,6 @@
<div class="step2-course-intro">
<div class="intro-header">
<span class="title">课程介绍</span>
<a-tag color="blue">已填写 {{ filledCount }} / 8</a-tag>
</div>
<a-tabs v-model:activeKey="activeTab" type="card">
@ -176,20 +175,6 @@ watch(
// tab
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
const prevTab = () => {
const index = currentTabIndex.value;
@ -221,7 +206,6 @@ const validate = () => {
defineExpose({
validate,
formData,
filledCount,
});
</script>