kindergarten_java/reading-platform-frontend/src/components/course/LessonStepsEditor.vue

341 lines
7.4 KiB
Vue
Raw Normal View History

2026-02-28 16:41:39 +08:00
<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>
2026-02-28 16:41:39 +08:00
<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>
2026-02-28 16:41:39 +08:00
<a-textarea
v-model:value="step.content"
placeholder="请输入教学环节的具体内容和操作步骤"
:rows="3"
@change="handleChange"
/>
</div>
<div class="step-objective">
<span class="field-label required">教学目标</span>
2026-02-28 16:41:39 +08:00
<a-input
v-model:value="step.objective"
placeholder="请输入该环节的教学目标"
2026-02-28 16:41:39 +08:00
@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;
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;
}
}
2026-02-28 16:41:39 +08:00
.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;
}
}
2026-02-28 16:41:39 +08:00
.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;
}
2026-02-28 16:41:39 +08:00
}
.step-objective {
.field-label {
margin-bottom: 4px;
}
2026-02-28 16:41:39 +08:00
:deep(.ant-input-affix-wrapper) {
background: #f5f5f5;
}
}
.template-section {
margin-top: 16px;
text-align: center;
}
}
</style>