360 lines
10 KiB
Vue
360 lines
10 KiB
Vue
<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>
|