kindergarten_java/reading-platform-frontend/src/components/course-edit/Step1BasicInfo.vue
2026-03-24 16:58:27 +08:00

360 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="step1-basic-info">
<a-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="课程包名称" name="name" required>
<a-input
v-model:value="formData.name"
placeholder="请输入课程包名称"
show-count
:maxlength="50"
@change="handleChange"
/>
</a-form-item>
<a-form-item label="关联主题" name="themeId" 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="适用年级" name="grades" 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="关联绘本" name="pictureBookName">
<a-input
v-model:value="formData.pictureBookName"
placeholder="请输入关联绘本名称(可选)"
:maxlength="100"
@change="handleChange"
/>
</a-form-item>
<a-form-item label="核心内容" name="coreContent" 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="课程时长" name="duration">
<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="核心发展目标" name="domainTags">
<a-select
v-model:value="formData.domainTags"
mode="multiple"
show-search
placeholder="请选择核心发展目标(可多选)"
style="width: 100%"
:filter-option="filterDomainTagOption"
@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="课程封面" required>
<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支持 JPG、PNG 格式,最大 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, getFileUrl } 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 formRef = ref();
const themesLoading = ref(false);
const themes = ref<Theme[]>([]);
const coverImages = ref<any[]>([]);
/** 核心发展目标:叶子项 + 父级领域名,用于搜索时同时匹配子项与分组 */
const DOMAIN_TAG_OPTIONS: { group: string; value: string; label: string }[] = [
{ group: '健康', value: 'health_motor', label: '身体动作发展' },
{ group: '健康', value: 'health_hygiene', label: '生活习惯与能力' },
{ group: '语言', value: 'lang_listen', label: '倾听与表达' },
{ group: '语言', value: 'lang_read', label: '早期阅读' },
{ group: '社会', value: 'social_interact', label: '人际交往' },
{ group: '社会', value: 'social_adapt', label: '社会适应' },
{ group: '科学', value: 'science_explore', label: '科学探究' },
{ group: '科学', value: 'math_cog', label: '数学认知' },
{ group: '艺术', value: 'art_music', label: '音乐表现' },
{ group: '艺术', value: 'art_create', label: '美术创作' },
];
const filterDomainTagOption = (input: string, option: any) => {
if (!input?.trim()) return true;
const q = input.trim();
const key = option?.value ?? option?.key;
const row = DOMAIN_TAG_OPTIONS.find((o) => o.value === key);
if (row) {
return row.label.includes(q) || row.group.includes(q);
}
const label =
typeof option?.label === 'string'
? option.label
: option?.children?.[0]?.children ?? option?.children;
if (typeof label === 'string') {
return label.includes(q);
}
return true;
};
const formRules = {
name: [
{ required: true, message: '请输入课程包名称' },
{ max: 50, message: '课程包名称不能超过50个字' },
{
validator: (_: unknown, value: string) => {
if (value && value.trim().length === 0) {
return Promise.reject(new Error('请输入课程包名称'));
}
return Promise.resolve();
},
},
],
themeId: [{ required: true, message: '请选择关联主题' }],
grades: [
{
required: true,
message: '请至少选择一个适用年级',
type: 'array',
min: 1,
},
],
coreContent: [
{ required: true, message: '请输入核心内容' },
{ max: 200, message: '核心内容不能超过200个字' },
],
duration: [
{ type: 'number' as const, min: 5, max: 120, message: '课程时长需为 5-120 的整数' },
],
pictureBookName: [{ max: 100, message: '关联绘本名称不能超过100个字' }],
};
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 = [{
uid: '-1',
name: 'cover',
status: 'done',
url: getFileUrl(newVal.coverImagePath),
}];
} else {
coverImages.value = [];
}
}
},
{ 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;
coverImages.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: getFileUrl(result.filePath),
}];
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 = async () => {
try {
await formRef.value?.validate();
// 校验课程封面
if (!formData.coverImagePath) {
return { valid: false, errors: ['请上传课程封面'] };
}
return { valid: true, errors: [] as string[] };
} catch (err: any) {
const errorFields = err?.errorFields || [];
const errors = errorFields
.map((f: any) => f.errors?.[0])
.filter(Boolean) as string[];
return { valid: false, errors: errors.length ? 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>