Files
zhuzi-admin/src/views/classes/components/ClassBatchImport.vue
2025-09-15 23:55:27 +08:00

368 lines
9.7 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>
<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>