fix(前端): 兼容 blob 错误体并优化课程草稿保存
- 解析 responseType=blob 的错误响应,正确透传后端 message - 课程包创建保存草稿仅校验基本信息,并保存后回填 ID/实体切换到编辑态 - 主题编辑弹窗回填颜色字段,修复颜色输入框不回显 Made-with: Cursor
This commit is contained in:
parent
a8931c8708
commit
ad5963be79
@ -1,4 +1,4 @@
|
|||||||
import axios, {type AxiosRequestConfig, type AxiosResponse} from "axios";
|
import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建默认 Axios 实例
|
* 创建默认 Axios 实例
|
||||||
@ -14,6 +14,7 @@ const axiosInstance = axios.create({
|
|||||||
*/
|
*/
|
||||||
axiosInstance.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
async (response) => {
|
async (response) => {
|
||||||
|
// console.error("请求响应-1:", response);
|
||||||
const { data, config } = response;
|
const { data, config } = response;
|
||||||
|
|
||||||
// 处理 Blob 响应(Orval 默认使用 blob)
|
// 处理 Blob 响应(Orval 默认使用 blob)
|
||||||
@ -33,7 +34,7 @@ axiosInstance.interceptors.response.use(
|
|||||||
return jsonData.data;
|
return jsonData.data;
|
||||||
}
|
}
|
||||||
// 业务错误码,抛出错误
|
// 业务错误码,抛出错误
|
||||||
const error: any = new Error(jsonData.message || '请求失败');
|
const error: any = new Error(jsonData.message || "请求失败");
|
||||||
error.response = { data: jsonData, status: 200 };
|
error.response = { data: jsonData, status: 200 };
|
||||||
error.code = jsonData.code;
|
error.code = jsonData.code;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
@ -54,7 +55,7 @@ axiosInstance.interceptors.response.use(
|
|||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
// 业务错误码,抛出错误
|
// 业务错误码,抛出错误
|
||||||
const error: any = new Error(data.message || '请求失败');
|
const error: any = new Error(data.message || "请求失败");
|
||||||
error.response = { data, status: 200 };
|
error.response = { data, status: 200 };
|
||||||
error.code = data.code;
|
error.code = data.code;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
@ -62,10 +63,32 @@ axiosInstance.interceptors.response.use(
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
(error) => {
|
async (error) => {
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
const message = error.response?.data?.message || error.message;
|
// axios 在 responseType=blob 且非 2xx 时,error.response.data 往往是 Blob(即使实际是 JSON)
|
||||||
|
const maybeBlob = error.response?.data;
|
||||||
|
if (maybeBlob instanceof Blob) {
|
||||||
|
try {
|
||||||
|
const text = await maybeBlob.text();
|
||||||
|
const jsonData = JSON.parse(text);
|
||||||
|
if (jsonData && typeof jsonData === "object") {
|
||||||
|
// 统一把解析后的 JSON 写回,便于业务侧 error.response.data.message 读取
|
||||||
|
error.response.data = jsonData;
|
||||||
|
// 同步 Error.message,便于直接用 error.message 展示
|
||||||
|
if (
|
||||||
|
typeof (jsonData as any).message === "string" &&
|
||||||
|
(jsonData as any).message
|
||||||
|
) {
|
||||||
|
error.message = (jsonData as any).message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore: 不是 JSON 或解析失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = error.response?.data?.message || error.message;
|
||||||
|
// console.error("请求失败-1:", error);
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from "ant-design-vue";
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: '/api', // 使用 /api 作为统一前缀,超管端路径包含 /v1
|
baseURL: "/api", // 使用 /api 作为统一前缀,超管端路径包含 /v1
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
request.interceptors.request.use(
|
request.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
@ -21,18 +21,19 @@ request.interceptors.request.use(
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 响应拦截器
|
// 响应拦截器
|
||||||
request.interceptors.response.use(
|
request.interceptors.response.use(
|
||||||
(response: AxiosResponse) => {
|
(response: AxiosResponse) => {
|
||||||
|
// console.error("响应拦截器-1:", response);
|
||||||
const { data } = response;
|
const { data } = response;
|
||||||
// 如果是标准响应格式 { code, message, data }
|
// 如果是标准响应格式 { code, message, data }
|
||||||
if (typeof data === 'object' && data !== null && 'code' in data) {
|
if (typeof data === "object" && data !== null && "code" in data) {
|
||||||
// 业务错误码非 200 时抛出错误
|
// 业务错误码非 200 时抛出错误
|
||||||
if (data.code !== 200) {
|
if (data.code !== 200) {
|
||||||
const error: any = new Error(data.message || '请求失败');
|
const error: any = new Error(data.message || "请求失败");
|
||||||
error.response = response;
|
error.response = response;
|
||||||
error.code = data.code;
|
error.code = data.code;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
@ -44,6 +45,7 @@ request.interceptors.response.use(
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
// console.error("响应拦截器错误-1:", error);
|
||||||
const { response } = error;
|
const { response } = error;
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
@ -51,49 +53,49 @@ request.interceptors.response.use(
|
|||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 401:
|
case 401:
|
||||||
message.error('登录已过期,请重新登录');
|
message.error("登录已过期,请重新登录");
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem("user");
|
||||||
localStorage.removeItem('role');
|
localStorage.removeItem("role");
|
||||||
localStorage.removeItem('name');
|
localStorage.removeItem("name");
|
||||||
window.location.href = '/login';
|
window.location.href = "/login";
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403:
|
||||||
// 区分 token 过期/无效和权限不足的场景
|
// 区分 token 过期/无效和权限不足的场景
|
||||||
// 如果是 token 问题导致的 403,跳转到登录页
|
// 如果是 token 问题导致的 403,跳转到登录页
|
||||||
if (data && typeof data === 'object' && 'code' in data) {
|
if (data && typeof data === "object" && "code" in data) {
|
||||||
const errorCode = data.code;
|
const errorCode = data.code;
|
||||||
// token 过期或无效时跳转到登录页
|
// token 过期或无效时跳转到登录页
|
||||||
if (errorCode === 401 || errorCode === 403) {
|
if (errorCode === 401 || errorCode === 403) {
|
||||||
message.error(data.message || '登录已过期,请重新登录');
|
message.error(data.message || "登录已过期,请重新登录");
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem("user");
|
||||||
localStorage.removeItem('role');
|
localStorage.removeItem("role");
|
||||||
localStorage.removeItem('name');
|
localStorage.removeItem("name");
|
||||||
window.location.href = '/login';
|
window.location.href = "/login";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 其他情况视为权限不足,显示提示但不跳转
|
// 其他情况视为权限不足,显示提示但不跳转
|
||||||
message.error(data?.message || '没有权限访问');
|
message.error(data?.message || "没有权限访问");
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
message.error('请求的资源不存在');
|
message.error("请求的资源不存在");
|
||||||
break;
|
break;
|
||||||
case 500:
|
case 500:
|
||||||
message.error(data?.message || '服务器错误');
|
message.error(data?.message || "服务器错误");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
message.error(data?.message || '请求失败');
|
message.error(data?.message || "请求失败");
|
||||||
}
|
}
|
||||||
} else if (error.code === 'ECONNABORTED') {
|
} else if (error.code === "ECONNABORTED") {
|
||||||
message.error('请求超时');
|
message.error("请求超时");
|
||||||
} else {
|
} else {
|
||||||
message.error('网络错误');
|
message.error("网络错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 导出请求方法
|
// 导出请求方法
|
||||||
@ -105,11 +107,19 @@ export const http = {
|
|||||||
return request.get(url, config);
|
return request.get(url, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
post<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
return request.post(url, data, config);
|
return request.post(url, data, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
put<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
return request.put(url, data, config);
|
return request.put(url, data, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -117,7 +127,11 @@ export const http = {
|
|||||||
return request.delete(url, config);
|
return request.delete(url, config);
|
||||||
},
|
},
|
||||||
|
|
||||||
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
patch<T = any>(
|
||||||
|
url: string,
|
||||||
|
data?: any,
|
||||||
|
config?: AxiosRequestConfig,
|
||||||
|
): Promise<T> {
|
||||||
return request.patch(url, data, config);
|
return request.patch(url, data, config);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,48 +23,46 @@
|
|||||||
<a-spin size="large" tip="正在加载课程数据..." />
|
<a-spin size="large" tip="正在加载课程数据..." />
|
||||||
</div>
|
</div>
|
||||||
<a-card v-else :bordered="false" style="margin-top: 16px;">
|
<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="基本信息" />
|
||||||
<a-step title="课程介绍" />
|
<a-step title="课程介绍" />
|
||||||
<a-step title="排课参考" />
|
<a-step title="排课参考" />
|
||||||
<a-step title="导入课" />
|
<a-step title="导入课" />
|
||||||
<a-step title="集体课" />
|
<a-step title="集体课" />
|
||||||
<a-step title="领域课" />
|
<a-step title="领域课" />
|
||||||
<a-step title="环创建设" />
|
<a-step title="环创建设" />
|
||||||
</a-steps>
|
</a-steps>
|
||||||
|
|
||||||
<!-- 步骤内容 -->
|
<!-- 步骤内容 -->
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<!-- 步骤1:基本信息 -->
|
<!-- 步骤1:基本信息 -->
|
||||||
<Step1BasicInfo v-show="currentStep === 0" ref="step1Ref" v-model="formData.basic"
|
<Step1BasicInfo v-show="currentStep === 0" ref="step1Ref" v-model="formData.basic" @change="handleDataChange" />
|
||||||
@change="handleDataChange" />
|
|
||||||
|
|
||||||
<!-- 步骤2:课程介绍 -->
|
<!-- 步骤2:课程介绍 -->
|
||||||
<Step2CourseIntro v-show="currentStep === 1" ref="step2Ref" v-model="formData.intro"
|
<Step2CourseIntro v-show="currentStep === 1" ref="step2Ref" v-model="formData.intro"
|
||||||
@change="handleDataChange" />
|
@change="handleDataChange" />
|
||||||
|
|
||||||
<!-- 步骤3:排课参考 -->
|
<!-- 步骤3:排课参考 -->
|
||||||
<Step3ScheduleRef v-show="currentStep === 2" ref="step3Ref" v-model="formData.scheduleRefData"
|
<Step3ScheduleRef v-show="currentStep === 2" ref="step3Ref" v-model="formData.scheduleRefData"
|
||||||
@change="handleDataChange" />
|
@change="handleDataChange" />
|
||||||
|
|
||||||
<!-- 步骤4:导入课 -->
|
<!-- 步骤4:导入课 -->
|
||||||
<Step4IntroLesson v-show="currentStep === 3" ref="step4Ref" :course-id="courseId"
|
<Step4IntroLesson v-show="currentStep === 3" ref="step4Ref" :course-id="courseId" @change="handleDataChange" />
|
||||||
@change="handleDataChange" />
|
|
||||||
|
|
||||||
<!-- 步骤5:集体课 -->
|
<!-- 步骤5:集体课 -->
|
||||||
<Step5CollectiveLesson v-show="currentStep === 4" ref="step5Ref" :course-id="courseId"
|
<Step5CollectiveLesson v-show="currentStep === 4" ref="step5Ref" :course-id="courseId"
|
||||||
:course-name="formData.basic.name" @change="handleDataChange" />
|
:course-name="formData.basic.name" @change="handleDataChange" />
|
||||||
|
|
||||||
<!-- 步骤6:领域课 -->
|
<!-- 步骤6:领域课 -->
|
||||||
<Step6DomainLessons v-show="currentStep === 5" ref="step6Ref" :course-id="courseId"
|
<Step6DomainLessons v-show="currentStep === 5" ref="step6Ref" :course-id="courseId"
|
||||||
:course-name="formData.basic.name" @change="handleDataChange" />
|
:course-name="formData.basic.name" @change="handleDataChange" />
|
||||||
|
|
||||||
<!-- 步骤7:环创建设 -->
|
<!-- 步骤7:环创建设 -->
|
||||||
<Step7Environment v-show="currentStep === 6" ref="step7Ref" v-model="formData.environmentConstruction"
|
<Step7Environment v-show="currentStep === 6" ref="step7Ref" v-model="formData.environmentConstruction"
|
||||||
@change="handleDataChange" />
|
@change="handleDataChange" />
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -283,8 +281,19 @@ const handleSaveDraft = async () => {
|
|||||||
|
|
||||||
// 保存
|
// 保存
|
||||||
const handleSave = async (isDraft = false) => {
|
const handleSave = async (isDraft = false) => {
|
||||||
// 正式保存才校验当前步骤;保存草稿允许未完成配置,不校验
|
// 校验逻辑:
|
||||||
if (!isDraft) {
|
// - 保存草稿:仅校验「基本信息」(Step1),其余环节不校验(按原来逻辑允许未完成)
|
||||||
|
// - 正式保存:仍按原逻辑校验当前步骤
|
||||||
|
if (isDraft) {
|
||||||
|
const step1 = step1Ref.value;
|
||||||
|
if (step1?.validate) {
|
||||||
|
const result = await step1.validate();
|
||||||
|
if (!result?.valid) {
|
||||||
|
message.warning(result?.errors?.[0] || '请完成基本信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const ok = await validateCurrentStep();
|
const ok = await validateCurrentStep();
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
@ -332,18 +341,57 @@ const handleSave = async (isDraft = false) => {
|
|||||||
|
|
||||||
console.log('Saving course data...', { isDraft, isEdit: isEdit.value });
|
console.log('Saving course data...', { isDraft, isEdit: isEdit.value });
|
||||||
|
|
||||||
|
let savedCourse: any = null;
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
await updateCourse(courseId.value, courseData);
|
const res = await updateCourse(courseId.value, courseData) as any;
|
||||||
|
savedCourse = res?.data ?? res ?? null;
|
||||||
console.log('Course updated successfully');
|
console.log('Course updated successfully');
|
||||||
} else {
|
} else {
|
||||||
const res = await createCourse(courseData) as any;
|
const res = await createCourse(courseData) as any;
|
||||||
savedCourseId = res?.id ?? res?.data?.id; // 响应拦截器已返回 data.data,但也兼容直接返回完整响应
|
savedCourse = res?.data ?? res ?? null;
|
||||||
|
savedCourseId = savedCourse?.id ?? res?.id ?? res?.data?.id; // 兼容多种返回形态
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!savedCourseId) {
|
if (!savedCourseId) {
|
||||||
throw new Error('无法获取课程ID');
|
throw new Error('无法获取课程ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 草稿保存:接口若返回实体与 ID,则填充到当前编辑,并切换到编辑路由
|
||||||
|
// 这样后续环节(导入课/集体课/领域课等)能拿到 courseId 进行关联保存
|
||||||
|
if (isDraft && !isEdit.value) {
|
||||||
|
// 回填(尽量不破坏当前填写内容;这里只补齐后端回写字段)
|
||||||
|
if (savedCourse && typeof savedCourse === 'object') {
|
||||||
|
formData.basic.name = savedCourse.name ?? formData.basic.name;
|
||||||
|
formData.basic.themeId = savedCourse.themeId ?? formData.basic.themeId;
|
||||||
|
formData.basic.pictureBookName = savedCourse.pictureBookName ?? formData.basic.pictureBookName;
|
||||||
|
formData.basic.coreContent = savedCourse.coreContent ?? formData.basic.coreContent;
|
||||||
|
formData.basic.duration = (savedCourse.durationMinutes ?? savedCourse.duration) ?? formData.basic.duration;
|
||||||
|
formData.basic.coverImagePath = savedCourse.coverImagePath ?? formData.basic.coverImagePath;
|
||||||
|
|
||||||
|
const gradeTags = savedCourse.gradeTags;
|
||||||
|
if (gradeTags !== undefined && gradeTags !== null) {
|
||||||
|
formData.basic.grades = Array.isArray(gradeTags) ? gradeTags : (gradeTags ? JSON.parse(gradeTags) : []);
|
||||||
|
}
|
||||||
|
const domainTags = savedCourse.domainTags;
|
||||||
|
if (domainTags !== undefined && domainTags !== null) {
|
||||||
|
formData.basic.domainTags = Array.isArray(domainTags) ? domainTags : (domainTags ? JSON.parse(domainTags || '[]') : []);
|
||||||
|
}
|
||||||
|
|
||||||
|
formData.intro.introSummary = savedCourse.introSummary ?? formData.intro.introSummary;
|
||||||
|
formData.intro.introHighlights = savedCourse.introHighlights ?? formData.intro.introHighlights;
|
||||||
|
formData.intro.introGoals = savedCourse.introGoals ?? formData.intro.introGoals;
|
||||||
|
formData.intro.introSchedule = savedCourse.introSchedule ?? formData.intro.introSchedule;
|
||||||
|
formData.intro.introKeyPoints = savedCourse.introKeyPoints ?? formData.intro.introKeyPoints;
|
||||||
|
formData.intro.introMethods = savedCourse.introMethods ?? formData.intro.introMethods;
|
||||||
|
formData.intro.introEvaluation = savedCourse.introEvaluation ?? formData.intro.introEvaluation;
|
||||||
|
formData.intro.introNotes = savedCourse.introNotes ?? formData.intro.introNotes;
|
||||||
|
formData.scheduleRefData = savedCourse.scheduleRefData ?? formData.scheduleRefData;
|
||||||
|
formData.environmentConstruction = savedCourse.environmentConstruction ?? formData.environmentConstruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
await router.replace(`/admin/packages/${savedCourseId}/edit`);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 保存导入课
|
// 2. 保存导入课
|
||||||
try {
|
try {
|
||||||
const introLessonData = step4Ref.value?.getSaveData();
|
const introLessonData = step4Ref.value?.getSaveData();
|
||||||
|
|||||||
@ -173,6 +173,7 @@ const handleEdit = (record: any) => {
|
|||||||
editingId.value = record.id;
|
editingId.value = record.id;
|
||||||
form.name = record.name;
|
form.name = record.name;
|
||||||
form.description = record.description || '';
|
form.description = record.description || '';
|
||||||
|
form.color = (record.color || '').toString();
|
||||||
form.sortOrder = record.sortOrder;
|
form.sortOrder = record.sortOrder;
|
||||||
modalVisible.value = true;
|
modalVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user