301 lines
6.6 KiB
Vue
301 lines
6.6 KiB
Vue
|
|
<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>
|
|||
|
|
<a-input
|
|||
|
|
v-model:value="step.name"
|
|||
|
|
placeholder="环节名称(如:导入、讲解、练习等)"
|
|||
|
|
style="flex: 1"
|
|||
|
|
@change="handleChange"
|
|||
|
|
/>
|
|||
|
|
<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">
|
|||
|
|
<a-textarea
|
|||
|
|
v-model:value="step.content"
|
|||
|
|
placeholder="请输入教学环节的具体内容和操作步骤"
|
|||
|
|
:rows="3"
|
|||
|
|
@change="handleChange"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="step-objective">
|
|||
|
|
<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;
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.step-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
margin-bottom: 12px;
|
|||
|
|
|
|||
|
|
.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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.step-objective {
|
|||
|
|
:deep(.ant-input-affix-wrapper) {
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.template-section {
|
|||
|
|
margin-top: 16px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|