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="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>
|
||||||
|
|
||||||
|
|||||||
@ -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,避免 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 预览)
|
// 检查是否为 PDF 文件(用于 ppt 类型也支持 PDF 预览)
|
||||||
const isPdfFile = computed(() => {
|
const isPdfFile = computed(() => {
|
||||||
if (props.fileType === 'ppt' && props.filePath) {
|
if (props.fileType === 'ppt' && props.filePath) {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user