368 lines
9.7 KiB
Vue
368 lines
9.7 KiB
Vue
<template>
|
||
<a-modal
|
||
:open="open"
|
||
title="批量导入班级"
|
||
@ok="handleImport"
|
||
@cancel="handleCancel"
|
||
:confirm-loading="loading"
|
||
width="600px"
|
||
>
|
||
<div class="batch-import">
|
||
<a-steps :current="currentStep" size="small">
|
||
<a-step title="下载模板" description="下载Excel导入模板" />
|
||
<a-step title="上传文件" description="上传填写好的Excel文件" />
|
||
<a-step title="确认导入" description="检查数据并确认导入" />
|
||
</a-steps>
|
||
|
||
<div class="step-content">
|
||
<!-- 步骤1: 下载模板 -->
|
||
<div v-if="currentStep === 0" class="step-download">
|
||
<a-alert
|
||
message="请先下载导入模板"
|
||
description="下载Excel模板文件,按照模板格式填写班级信息"
|
||
type="info"
|
||
show-icon
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
|
||
<div class="template-info">
|
||
<h4>模板说明:</h4>
|
||
<ul>
|
||
<li>班级名称:必填,如"一年级1班"</li>
|
||
<li>班级代码:必填,如"CLASS_1_1"</li>
|
||
<li>班主任姓名:选填</li>
|
||
<li>班主任电话:选填,格式为11位手机号</li>
|
||
<li>教室地址:选填</li>
|
||
<li>最大学生数:选填,默认40人</li>
|
||
<li>入学年份:选填,格式为YYYY</li>
|
||
<li>班级描述:选填</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div class="template-actions">
|
||
<a-button type="primary" @click="downloadTemplate">
|
||
<DownloadOutlined />
|
||
下载导入模板
|
||
</a-button>
|
||
<a-button @click="nextStep" style="margin-left: 8px">
|
||
下一步
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤2: 上传文件 -->
|
||
<div v-if="currentStep === 1" class="step-upload">
|
||
<a-alert
|
||
message="上传Excel文件"
|
||
description="请选择填写好的Excel文件进行上传"
|
||
type="info"
|
||
show-icon
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
|
||
<a-upload-dragger
|
||
v-model:file-list="fileList"
|
||
name="file"
|
||
:multiple="false"
|
||
accept=".xlsx,.xls"
|
||
:before-upload="handleBeforeUpload"
|
||
@change="handleFileChange"
|
||
>
|
||
<p class="ant-upload-drag-icon">
|
||
<InboxOutlined />
|
||
</p>
|
||
<p class="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
||
<p class="ant-upload-hint">
|
||
支持.xlsx、.xls格式,文件大小不超过10MB
|
||
</p>
|
||
</a-upload-dragger>
|
||
|
||
<div class="upload-actions">
|
||
<a-button @click="prevStep">
|
||
上一步
|
||
</a-button>
|
||
<a-button
|
||
type="primary"
|
||
@click="nextStep"
|
||
:disabled="!uploadFile"
|
||
style="margin-left: 8px"
|
||
>
|
||
下一步
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 步骤3: 确认导入 -->
|
||
<div v-if="currentStep === 2" class="step-confirm">
|
||
<a-alert
|
||
message="确认导入信息"
|
||
description="请确认要导入的班级信息"
|
||
type="warning"
|
||
show-icon
|
||
style="margin-bottom: 16px"
|
||
/>
|
||
|
||
<div class="import-info">
|
||
<div class="info-item">
|
||
<span class="label">学校:</span>
|
||
<span class="value">{{ schoolName }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">年级:</span>
|
||
<span class="value">{{ gradeName }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">文件名:</span>
|
||
<span class="value">{{ uploadFile?.name }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">文件大小:</span>
|
||
<span class="value">{{ formatFileSize(uploadFile?.size || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="confirm-actions">
|
||
<a-button @click="prevStep">
|
||
上一步
|
||
</a-button>
|
||
<a-button
|
||
type="primary"
|
||
@click="handleImport"
|
||
:loading="loading"
|
||
style="margin-left: 8px"
|
||
>
|
||
确认导入
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue';
|
||
import { message } from 'ant-design-vue';
|
||
import {
|
||
DownloadOutlined,
|
||
InboxOutlined
|
||
} from '@ant-design/icons-vue';
|
||
import { importClasses } from '@/apis/classes';
|
||
|
||
interface Props {
|
||
open: boolean;
|
||
schoolId: string;
|
||
gradeId: string;
|
||
}
|
||
|
||
interface Emits {
|
||
(e: 'update:open', value: boolean): void;
|
||
(e: 'success'): void;
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
const emit = defineEmits<Emits>();
|
||
|
||
// 数据
|
||
const loading = ref(false);
|
||
const currentStep = ref(0);
|
||
const fileList = ref([]);
|
||
const uploadFile = ref<File | null>(null);
|
||
|
||
// 计算属性
|
||
const schoolName = computed(() => '朱熹小学'); // 这里应该根据schoolId获取学校名称
|
||
const gradeName = computed(() => '一年级'); // 这里应该根据gradeId获取年级名称
|
||
|
||
// 方法
|
||
const downloadTemplate = () => {
|
||
// 创建模板数据
|
||
const templateData = [
|
||
['班级名称', '班级代码', '班主任姓名', '班主任电话', '教室地址', '最大学生数', '入学年份', '班级描述'],
|
||
['一年级1班', 'CLASS_1_1', '张老师', '13800138001', '教学楼A101', '40', '2024', '示例班级描述'],
|
||
['一年级2班', 'CLASS_1_2', '李老师', '13800138002', '教学楼A102', '40', '2024', '示例班级描述']
|
||
];
|
||
|
||
// 创建CSV内容
|
||
const csvContent = templateData.map(row => row.join(',')).join('\n');
|
||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||
|
||
// 下载文件
|
||
const link = document.createElement('a');
|
||
const url = URL.createObjectURL(blob);
|
||
link.setAttribute('href', url);
|
||
link.setAttribute('download', '班级导入模板.csv');
|
||
link.style.visibility = 'hidden';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
message.success('模板下载成功');
|
||
};
|
||
|
||
const handleBeforeUpload = (file: File) => {
|
||
const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||
file.type === 'application/vnd.ms-excel' ||
|
||
file.name.endsWith('.xlsx') ||
|
||
file.name.endsWith('.xls');
|
||
|
||
if (!isExcel) {
|
||
message.error('只能上传Excel文件(.xlsx或.xls)');
|
||
return false;
|
||
}
|
||
|
||
const isLt10M = file.size / 1024 / 1024 < 10;
|
||
if (!isLt10M) {
|
||
message.error('文件大小不能超过10MB');
|
||
return false;
|
||
}
|
||
|
||
uploadFile.value = file;
|
||
return false; // 阻止自动上传
|
||
};
|
||
|
||
const handleFileChange = (info: any) => {
|
||
const { fileList: newFileList } = info;
|
||
fileList.value = newFileList.slice(-1); // 只保留最新的一个文件
|
||
};
|
||
|
||
const handleImport = async () => {
|
||
if (!uploadFile.value) {
|
||
message.error('请先上传文件');
|
||
return;
|
||
}
|
||
|
||
if (!props.schoolId || !props.gradeId) {
|
||
message.error('请先选择学校和年级');
|
||
return;
|
||
}
|
||
|
||
loading.value = true;
|
||
try {
|
||
const result: any = await importClasses({
|
||
schoolId: props.schoolId,
|
||
gradeId: props.gradeId,
|
||
file: uploadFile.value
|
||
});
|
||
|
||
if (result.success === result.total) {
|
||
message.success(`导入成功,共导入 ${result.success} 个班级`);
|
||
} else {
|
||
message.warning(`导入完成,成功 ${result.success} 个,失败 ${result.failed} 个`);
|
||
}
|
||
|
||
emit('success');
|
||
} catch (error) {
|
||
console.error('批量导入失败:', error);
|
||
message.error('批量导入失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
const nextStep = () => {
|
||
if (currentStep.value < 2) {
|
||
currentStep.value++;
|
||
}
|
||
};
|
||
|
||
const prevStep = () => {
|
||
if (currentStep.value > 0) {
|
||
currentStep.value--;
|
||
}
|
||
};
|
||
|
||
const handleCancel = () => {
|
||
emit('update:open', false);
|
||
resetForm();
|
||
};
|
||
|
||
const resetForm = () => {
|
||
currentStep.value = 0;
|
||
fileList.value = [];
|
||
uploadFile.value = null;
|
||
};
|
||
|
||
const formatFileSize = (size: number) => {
|
||
if (size < 1024) {
|
||
return size + ' B';
|
||
} else if (size < 1024 * 1024) {
|
||
return (size / 1024).toFixed(2) + ' KB';
|
||
} else {
|
||
return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.batch-import {
|
||
.step-content {
|
||
margin-top: 24px;
|
||
min-height: 300px;
|
||
}
|
||
|
||
.step-download {
|
||
.template-info {
|
||
margin: 16px 0;
|
||
padding: 16px;
|
||
background: #f6f8fa;
|
||
border-radius: 6px;
|
||
|
||
h4 {
|
||
margin: 0 0 8px 0;
|
||
color: #333;
|
||
}
|
||
|
||
ul {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
|
||
li {
|
||
margin-bottom: 4px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
.template-actions {
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.step-upload {
|
||
.upload-actions {
|
||
margin-top: 16px;
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
.step-confirm {
|
||
.import-info {
|
||
margin: 16px 0;
|
||
padding: 16px;
|
||
background: #f6f8fa;
|
||
border-radius: 6px;
|
||
|
||
.info-item {
|
||
display: flex;
|
||
margin-bottom: 8px;
|
||
|
||
.label {
|
||
width: 80px;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.value {
|
||
flex: 1;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.confirm-actions {
|
||
text-align: right;
|
||
}
|
||
}
|
||
}
|
||
</style>
|