fix(admin): 课程编辑加载与表单告警;FileUploader 预览 watch 顺序;教学环节 Form.ItemRest
Made-with: Cursor
This commit is contained in:
parent
94ea219f2f
commit
c1ee18ca97
@ -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>
|
||||
|
||||
|
||||
@ -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,避免 TDZ(Cannot 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) {
|
||||
|
||||
@ -25,7 +25,9 @@
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<span class="duration-unit">分钟</span>
|
||||
<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">
|
||||
<LessonStepsEditor
|
||||
v-model="lessonData.steps"
|
||||
:show-template="showTemplate"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<a-form-item-rest>
|
||||
<LessonStepsEditor
|
||||
v-model="lessonData.steps"
|
||||
:show-template="showTemplate"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</a-form-item-rest>
|
||||
</a-form-item>
|
||||
</a-card>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user