kindergarten_java/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue

296 lines
8.0 KiB
Vue
Raw Normal View History

refactor: 完成代码重构规范化 - 2026-03-12 后端重构: - 添加统一响应格式 ResultDto<T> 和 PageResultDto<T> - 添加分页查询 DTO 基类 PageQueryDto - 添加响应转换拦截器 TransformInterceptor - 添加公共工具函数(JSON 解析、分页计算) - 配置 Swagger/OpenAPI 文档(访问路径:/api-docs) - Tenant 模块 DTO 规范化示例(添加 @ApiProperty 装饰器) - CourseLesson 控制器重构 - 移除类级路径参数,修复 Orval 验证错误 - 后端 DTO 规范化 - 为 Course、Lesson、TeacherCourse、SchoolCourse 控制器添加完整的 Swagger 装饰器 前端重构: - 配置 Orval 从后端 OpenAPI 自动生成 API 客户端 - 生成 API 客户端代码(带完整参数定义) - 创建 API 客户端统一入口 (src/api/client.ts) - 创建 API 适配层 (src/api/teacher.adapter.ts) - 配置文件路由 (unplugin-vue-router) - 课程模块迁移到新 API 客户端 - 修复 PrepareModeView.vue API 调用错误 - 教师模块迁移到新 API 客户端 - 修复 school-course.ts 类型错误 - 清理 teacher.adapter.ts 未使用导入 - 修复 client.ts API 客户端结构 - 创建文件路由目录结构 Bug 修复: - 修复路由配置问题 - 移除 top-level await,改用手动路由配置 - 修复响应拦截器 - 正确解包 { code, message, data } 格式的响应 - 清理 teacher.adapter.ts 未使用导入 - 修复 client.ts API 客户端结构 - 创建文件路由目录结构 系统测试: - 后端 API 测试通过 (7/7) - 前端路由测试通过 (4/4) - 数据库完整性验证通过 - Orval API 客户端验证通过 - 超管端功能测试通过 (97.8% 通过率) 新增文件: - reading-platform-backend/src/common/dto/result.dto.ts - reading-platform-backend/src/common/dto/page-query.dto.ts - reading-platform-backend/src/common/interceptors/transform.interceptor.ts - reading-platform-backend/src/common/utils/json.util.ts - reading-platform-backend/src/common/utils/pagination.util.ts - reading-platform-frontend/orval.config.ts - reading-platform-frontend/src/api/generated/mutator.ts - reading-platform-frontend/src/api/client.ts - reading-platform-frontend/src/api/teacher.adapter.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:27:13 +08:00
<template>
<div class="step1-basic-info">
<a-form
:model="formData"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="课程包名称" required>
<a-input
v-model:value="formData.name"
placeholder="请输入课程包名称"
show-count
:maxlength="50"
@change="handleChange"
/>
</a-form-item>
<a-form-item label="关联主题" required>
<a-select
v-model:value="formData.themeId"
placeholder="请选择主题"
style="width: 100%"
:loading="themesLoading"
@change="handleChange"
>
<a-select-option v-for="theme in themes" :key="theme.id" :value="theme.id">
{{ theme.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="适用年级" required>
<a-checkbox-group v-model:value="formData.grades" @change="handleChange">
<a-checkbox value="小班">小班</a-checkbox>
<a-checkbox value="中班">中班</a-checkbox>
<a-checkbox value="大班">大班</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="关联绘本">
<a-input
v-model:value="formData.pictureBookName"
placeholder="请输入关联绘本名称(可选)"
@change="handleChange"
/>
</a-form-item>
<a-form-item label="核心内容" required>
<a-textarea
v-model:value="formData.coreContent"
:rows="3"
placeholder="请输入课程包核心内容概述200字以内"
show-count
:maxlength="200"
@change="handleChange"
/>
</a-form-item>
<a-form-item label="课程时长">
<a-input-number
v-model:value="formData.duration"
:min="5"
:max="120"
style="width: 150px"
@change="handleChange"
/>
<span class="duration-unit">分钟</span>
</a-form-item>
<a-form-item label="核心发展目标">
<a-select
v-model:value="formData.domainTags"
mode="multiple"
placeholder="请选择核心发展目标(可多选)"
style="width: 100%"
@change="handleChange"
>
<a-select-opt-group label="健康">
<a-select-option value="health_motor">身体动作发展</a-select-option>
<a-select-option value="health_hygiene">生活习惯与能力</a-select-option>
</a-select-opt-group>
<a-select-opt-group label="语言">
<a-select-option value="lang_listen">倾听与表达</a-select-option>
<a-select-option value="lang_read">早期阅读</a-select-option>
</a-select-opt-group>
<a-select-opt-group label="社会">
<a-select-option value="social_interact">人际交往</a-select-option>
<a-select-option value="social_adapt">社会适应</a-select-option>
</a-select-opt-group>
<a-select-opt-group label="科学">
<a-select-option value="science_explore">科学探究</a-select-option>
<a-select-option value="math_cog">数学认知</a-select-option>
</a-select-opt-group>
<a-select-opt-group label="艺术">
<a-select-option value="art_music">音乐表现</a-select-option>
<a-select-option value="art_create">美术创作</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-item>
<a-form-item label="课程封面">
<a-upload
v-model:file-list="coverImages"
list-type="picture-card"
:max-count="1"
accept="image/*"
:before-upload="beforeCoverUpload"
@remove="handleCoverRemove"
>
<div v-if="coverImages.length < 1">
<PlusOutlined />
<div style="margin-top: 8px">上传封面</div>
</div>
</a-upload>
<div class="upload-hint">建议尺寸 400x300支持 JPGPNG 格式最大 5MB</div>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { getThemeList } from '@/api/theme';
import { uploadFile } from '@/api/file';
import type { Theme } from '@/api/theme';
interface BasicInfoData {
name: string;
themeId: number | undefined;
grades: string[];
pictureBookName: string;
coreContent: string;
duration: number;
domainTags: string[];
coverImagePath: string;
}
interface Props {
modelValue: Partial<BasicInfoData>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:modelValue', value: BasicInfoData): void;
(e: 'change'): void;
}>();
const themesLoading = ref(false);
const themes = ref<Theme[]>([]);
const coverImages = ref<any[]>([]);
const formData = reactive<BasicInfoData>({
name: '',
themeId: undefined,
grades: [],
pictureBookName: '',
coreContent: '',
duration: 25,
domainTags: [],
coverImagePath: '',
});
// 监听外部值变化
watch(
() => props.modelValue,
(newVal) => {
if (newVal) {
Object.assign(formData, newVal);
// 处理封面图片
if (newVal.coverImagePath && coverImages.value.length === 0) {
// 构建正确的图片URL
let imageUrl = newVal.coverImagePath;
if (!imageUrl.startsWith('http') && !imageUrl.startsWith('/uploads') && !imageUrl.includes('/uploads/')) {
imageUrl = `/uploads/${imageUrl}`;
}
coverImages.value = [{
uid: '-1',
name: 'cover',
status: 'done',
url: imageUrl,
}];
}
}
},
{ immediate: true, deep: true }
);
// 获取主题列表
const fetchThemes = async () => {
themesLoading.value = true;
try {
const res = await getThemeList() as any;
themes.value = res || [];
} catch (error) {
console.error('获取主题列表失败', error);
} finally {
themesLoading.value = false;
}
};
// 封面上传
const beforeCoverUpload = async (file: any) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件');
return false;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('图片大小不能超过 5MB');
return false;
}
try {
const result = await uploadFile(file, 'cover');
formData.coverImagePath = result.filePath;
// 构建正确的图片URL - 后端返回的filePath已经包含完整路径
let imageUrl = result.filePath;
if (!imageUrl.startsWith('http') && !imageUrl.startsWith('/uploads') && !imageUrl.includes('/uploads/')) {
imageUrl = `/uploads/${imageUrl}`;
}
coverImages.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: imageUrl,
}];
handleChange();
message.success('封面上传成功');
} catch (error: any) {
message.error('上传失败: ' + (error.response?.data?.message || error.message));
}
return false;
};
const handleCoverRemove = () => {
coverImages.value = [];
formData.coverImagePath = '';
handleChange();
};
// 处理变化
const handleChange = () => {
emit('update:modelValue', { ...formData });
emit('change');
};
// 验证
const validate = () => {
const errors: string[] = [];
if (!formData.name) {
errors.push('请输入课程包名称');
}
if (!formData.themeId) {
errors.push('请选择关联主题');
}
if (formData.grades.length === 0) {
errors.push('请选择适用年级');
}
if (!formData.coreContent) {
errors.push('请输入核心内容');
}
return { valid: errors.length === 0, errors };
};
onMounted(() => {
fetchThemes();
});
defineExpose({
validate,
formData,
});
</script>
<style scoped lang="scss">
.step1-basic-info {
.duration-unit {
margin-left: 8px;
color: #666;
}
.upload-hint {
margin-top: 8px;
font-size: 12px;
color: #999;
}
}
</style>