kindergarten_java/reading-platform-frontend/src/components/course/LessonStepsEditor.vue
En 55343ead0b fix(前端): 修复 ID 类型和分页 total 类型不匹配问题
- 将 Props 中 ID 字段从 number 改为 number | string,兼容后端 Long 序列化为 String
- 修复分页组件 total 字段类型,使用 Number() 转换避免 Vue warn
- 影响组件: PrepareNavigation, LessonCard, SelectLessonsModal 等
- 影响视图: StudentListView, TeacherListView, ParentListView 等
2026-03-25 10:47:19 +08:00

341 lines
7.4 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="lesson-steps-editor">
<div class="steps-header">
<span class="title">教学环节 ({{ steps.length }})</span>
<a-button type="primary" size="small" @click="addStep">
<PlusOutlined /> 添加环节
</a-button>
</div>
<div v-if="steps.length === 0" class="empty-steps">
<a-empty description="暂无教学环节,请点击添加" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
<div v-else class="steps-list">
<div
v-for="(step, index) in steps"
:key="step.id || step.tempId"
class="step-item"
>
<div class="step-header">
<span class="step-order">{{ index + 1 }}</span>
<div class="step-name-field">
<span class="field-label required">环节名称</span>
<a-input
v-model:value="step.name"
placeholder="如:导入、讲解、练习等"
style="flex: 1"
@change="handleChange"
/>
</div>
<span class="step-duration">
<a-input-number
v-model:value="step.duration"
:min="1"
:max="60"
size="small"
style="width: 70px"
@change="handleChange"
/>
分钟
</span>
<a-popconfirm
title="确定删除此环节吗?"
@confirm="removeStep(index)"
>
<a-button type="link" size="small" danger>
<DeleteOutlined />
</a-button>
</a-popconfirm>
</div>
<div class="step-content">
<span class="field-label required">环节内容</span>
<a-textarea
v-model:value="step.content"
placeholder="请输入教学环节的具体内容和操作步骤"
:rows="3"
@change="handleChange"
/>
</div>
<div class="step-objective">
<span class="field-label required">教学目标</span>
<a-input
v-model:value="step.objective"
placeholder="请输入该环节的教学目标"
@change="handleChange"
>
<template #prefix>
<AimOutlined style="color: #bfbfbf" />
</template>
</a-input>
</div>
</div>
</div>
<!-- 4环节模板按钮可选 -->
<div v-if="showTemplate && steps.length === 0" class="template-section">
<a-divider>或使用模板</a-divider>
<a-button type="dashed" block @click="applyTemplate">
<AppstoreAddOutlined /> 应用4环节教学模板
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { Empty } from 'ant-design-vue';
import {
PlusOutlined,
DeleteOutlined,
AimOutlined,
AppstoreAddOutlined,
} from '@ant-design/icons-vue';
export interface StepData {
id?: number | string;
tempId?: string;
name: string;
content: string;
duration: number;
objective: string;
isNew?: boolean;
}
interface Props {
modelValue: StepData[];
showTemplate?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => [],
showTemplate: false,
});
const emit = defineEmits<{
(e: 'update:modelValue', value: StepData[]): void;
(e: 'change'): void;
}>();
const steps = ref<StepData[]>([]);
// 监听外部值变化
watch(
() => props.modelValue,
(newVal) => {
steps.value = newVal ? [...newVal] : [];
},
{ immediate: true, deep: true }
);
// 计算总时长
const totalDuration = computed(() => {
return steps.value.reduce((sum, step) => sum + (step.duration || 0), 0);
});
// 生成临时ID
const generateTempId = () => `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 添加环节
const addStep = () => {
const newStep: StepData = {
tempId: generateTempId(),
name: '',
content: '',
duration: 5,
objective: '',
isNew: true,
};
steps.value.push(newStep);
emitChange();
};
// 删除环节
const removeStep = (index: number) => {
steps.value.splice(index, 1);
emitChange();
};
// 应用4环节模板
const applyTemplate = () => {
const template: StepData[] = [
{
tempId: generateTempId(),
name: '导入环节',
content: '通过图片、视频或问题导入,激发幼儿兴趣',
duration: 3,
objective: '激发兴趣,引入主题',
isNew: true,
},
{
tempId: generateTempId(),
name: '展开环节',
content: '讲解绘本内容,引导幼儿观察、思考和表达',
duration: 10,
objective: '理解内容,发展语言能力',
isNew: true,
},
{
tempId: generateTempId(),
name: '深入环节',
content: '通过互动、讨论或游戏加深理解',
duration: 8,
objective: '深化理解,培养思维',
isNew: true,
},
{
tempId: generateTempId(),
name: '结束环节',
content: '总结回顾,延伸活动介绍',
duration: 4,
objective: '巩固学习,自然结束',
isNew: true,
},
];
steps.value = template;
emitChange();
};
// 处理变化
const handleChange = () => {
emitChange();
};
// 发送更新
const emitChange = () => {
emit('update:modelValue', [...steps.value]);
emit('change');
};
// 暴露方法
defineExpose({
totalDuration,
addStep,
removeStep,
});
</script>
<style scoped lang="scss">
.lesson-steps-editor {
.steps-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.title {
font-weight: 500;
font-size: 14px;
}
}
.empty-steps {
padding: 30px 0;
text-align: center;
background: #fafafa;
border-radius: 8px;
}
.steps-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.step-item {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 16px;
transition: all 0.3s;
&:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
}
}
.field-label {
display: block;
font-size: 13px;
color: #666;
margin-bottom: 4px;
&.required::after {
content: ' *';
color: #ff4d4f;
}
}
.step-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
.step-name-field {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
min-width: 0;
.field-label {
flex-shrink: 0;
margin-bottom: 0;
white-space: nowrap;
}
}
.step-order {
width: 28px;
height: 28px;
border-radius: 50%;
background: linear-gradient(135deg, #1890ff, #40a9ff);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
flex-shrink: 0;
}
.step-duration {
display: flex;
align-items: center;
gap: 4px;
color: #666;
font-size: 13px;
white-space: nowrap;
}
}
.step-content {
margin-bottom: 12px;
.field-label {
margin-bottom: 4px;
}
}
.step-objective {
.field-label {
margin-bottom: 4px;
}
:deep(.ant-input-affix-wrapper) {
background: #f5f5f5;
}
}
.template-section {
margin-top: 16px;
text-align: center;
}
}
</style>