提交 e71b0b3f authored 作者: 王鹏飞's avatar 王鹏飞

feat: 教工、专业、学期支持批量导入

上级 56a6d7ea
差异被折叠。
...@@ -28,7 +28,8 @@ ...@@ -28,7 +28,8 @@
"video.js": "^7.19.2", "video.js": "^7.19.2",
"vue": "^3.2.37", "vue": "^3.2.37",
"vue-echarts": "^7.0.3", "vue-echarts": "^7.0.3",
"vue-router": "^4.0.16" "vue-router": "^4.0.16",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.3", "@rushstack/eslint-patch": "^1.1.3",
...@@ -36,6 +37,7 @@ ...@@ -36,6 +37,7 @@
"@types/node": "^17.0.35", "@types/node": "^17.0.35",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/sortablejs": "^1.13.0", "@types/sortablejs": "^1.13.0",
"@types/xlsx": "^0.0.35",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^3.2.0",
"@vue/eslint-config-typescript": "^10.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.1.3",
......
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { splitStrLast } from '@/utils/util'
import { createPro } from '../api'
import * as XLSX from 'xlsx'
import { useMapStore } from '@/stores/map'
const emit = defineEmits<Emits>()
const uploadRef = ref()
const fileList = ref([]) // 文件列表
const isShowImportDialog = ref(false)
const tableData = ref<any[]>([]) // 导入的数据
const store = useMapStore()
defineProps({
isShowImportDialog: {
type: Boolean,
},
})
interface Emits {
(e: 'update:isShowImportDialog', isShowImportDialog: boolean): void
(e: 'create'): void
}
// 获取字典数据
const categoryList = ref<any[]>([])
const educationList = ref<any[]>([])
const schoolingList = ref<any[]>([])
const degreeCategoryList = ref<any[]>([])
const specialtyDegreeList = ref<any[]>([])
// 打开导入弹框
const handleImport = () => {
isShowImportDialog.value = true
tableData.value = []
fileList.value = []
// 获取字典数据
categoryList.value = store.getMapValuesByKey('specialty_category')
educationList.value = store.getMapValuesByKey('specialty_education_background')
schoolingList.value = store.getMapValuesByKey('specialty_length_of_schooling')
degreeCategoryList.value = store.getMapValuesByKey('specialty_degree_category')
specialtyDegreeList.value = store.getMapValuesByKey('specialty_degree')
}
// 取消
const handleCancel = () => {
isShowImportDialog.value = false
tableData.value = []
fileList.value = []
}
// 文件上传前校验
const beforeUpload = (file: any) => {
const suffix = splitStrLast(file.name, '.')
if (!['xlsx', 'xls'].includes(suffix)) {
ElMessage.warning('只能上传excel文件')
return false
} else {
return true
}
}
// 读取Excel文件
const readExcel = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e: any) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
resolve(jsonData)
} catch (error) {
reject(error)
}
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
// 根据字典label查找value
const findValueByLabel = (list: any[], label: string) => {
const item = list.find((item: any) => item.label === label)
return item ? item.value : ''
}
// 解析Excel数据
const parseExcelData = (jsonData: any[]) => {
const result: any[] = []
jsonData.forEach((row: any, index: number) => {
// 根据Excel列名映射数据
const item: any = {
id: index + 1,
code: row['专业代码'] || row['code'] || '',
name: row['专业名称'] || row['name'] || '',
category: '',
category_name: row['专业类别'] || row['category_name'] || '',
education_background: '',
education_background_name: row['学历'] || row['education_background_name'] || '',
length_of_schooling: '',
length_of_schooling_name: row['学制'] || row['length_of_schooling_name'] || '',
degree_category: '',
degree_category_name: row['学位门类'] || row['degree_category_name'] || '',
degree: '',
degree_name: row['学位'] || row['degree_name'] || '',
status: 'pending', // 初始状态:待导入
errorMsg: '', // 错误信息
}
// 根据名称查找对应的value
if (item.category_name) {
item.category = findValueByLabel(categoryList.value, item.category_name)
}
if (item.education_background_name) {
item.education_background = findValueByLabel(educationList.value, item.education_background_name)
}
if (item.length_of_schooling_name) {
item.length_of_schooling = findValueByLabel(schoolingList.value, item.length_of_schooling_name)
}
if (item.degree_category_name) {
item.degree_category = findValueByLabel(degreeCategoryList.value, item.degree_category_name)
}
if (item.degree_name) {
item.degree = findValueByLabel(specialtyDegreeList.value, item.degree_name)
}
result.push(item)
})
return result
}
// 文件上传处理
const handleFileChange = async (file: any) => {
try {
const jsonData = await readExcel(file.raw)
tableData.value = parseExcelData(jsonData as any[])
ElMessage.success('Excel解析成功')
} catch (error) {
ElMessage.error('Excel解析失败,请检查文件格式')
console.error('Excel解析错误:', error)
}
}
// 批量导入
const handleBatchImport = async () => {
if (tableData.value.length === 0) {
ElMessage.warning('请先上传Excel文件')
return
}
// 验证数据
const invalidData = tableData.value.filter(
(item) =>
!item.code ||
!item.name ||
!item.category ||
!item.education_background ||
!item.length_of_schooling ||
!item.degree_category ||
!item.degree
)
if (invalidData.length > 0) {
ElMessage.warning('存在数据不完整,请检查')
return
}
let successCount = 0
let failCount = 0
// 逐条导入
for (const item of tableData.value) {
if (item.status === 'success') continue // 跳过已成功的
item.status = 'importing'
try {
const params: any = {
code: item.code,
name: item.name,
category: item.category,
education_background: item.education_background,
length_of_schooling: item.length_of_schooling,
degree_category: item.degree_category,
degree: item.degree,
status: '1',
}
await createPro(params)
item.status = 'success'
successCount++
} catch (error: any) {
item.status = 'failed'
item.errorMsg = error.message || '导入失败'
failCount++
}
}
// 显示导入结果
if (failCount === 0) {
ElMessage.success(`成功导入 ${successCount} 条数据`)
emit('create')
setTimeout(() => {
handleCancel()
}, 1500)
} else {
ElMessage.warning(`成功导入 ${successCount} 条,失败 ${failCount} 条`)
}
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: any = {
pending: '待导入',
importing: '导入中',
success: '成功',
failed: '失败',
}
return statusMap[status] || ''
}
// 获取状态类型
const getStatusType = (status: string) => {
const typeMap: any = {
pending: 'info',
importing: 'warning',
success: 'success',
failed: 'danger',
}
return typeMap[status] || 'info'
}
</script>
<template>
<el-button type="primary" round @click="handleImport">批量导入</el-button>
<el-dialog
draggable
v-model="isShowImportDialog"
:close-on-click-modal="false"
:before-close="handleCancel"
title="批量导入专业"
width="1000px">
<div>
<el-upload
style="text-align: center"
class="file-import"
ref="uploadRef"
action="#"
accept=".xls,.xlsx"
drag
:auto-upload="false"
:file-list="fileList"
:limit="1"
:before-upload="beforeUpload"
:on-change="handleFileChange">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖至此处,点击上传</div>
</el-upload>
<div style="margin-bottom: 10px; text-align: right">
<a href="/center_resource/专业导入模板.xlsx" download="专业导入模板">
<el-link type="primary">下载模板</el-link>
</a>
</div>
<!-- 数据预览表格 -->
<el-table v-if="tableData.length > 0" :data="tableData" border max-height="400" style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="code" label="专业代码" align="center" />
<el-table-column prop="name" label="专业名称" align="center" />
<el-table-column prop="category_name" label="专业类别" align="center" />
<el-table-column prop="education_background_name" label="学历" align="center" />
<el-table-column prop="length_of_schooling_name" label="学制" align="center" />
<el-table-column prop="degree_category_name" label="学位门类" align="center" />
<el-table-column prop="degree_name" label="学位" align="center" />
<el-table-column label="导入状态" align="center" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="错误信息" align="center" width="200">
<template #default="{ row }">
<span style="color: red">{{ row.errorMsg }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<span>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleBatchImport" :disabled="tableData.length === 0"> 开始导入 </el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.file-import {
margin-bottom: 20px;
}
</style>
...@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus' ...@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus'
import { getProList, updatePro } from '../api' import { getProList, updatePro } from '../api'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import AddPro from '../components/AddPro.vue' import AddPro from '../components/AddPro.vue'
import ImportPro from '../components/ImportPro.vue'
// 判断当前用户是不是超级管理员 // 判断当前用户是不是超级管理员
const user = useUserStore().roles const user = useUserStore().roles
const isAdmin = !!user.find((item: any) => item.name === '超级管理员') const isAdmin = !!user.find((item: any) => item.name === '超级管理员')
...@@ -26,8 +27,8 @@ const listOptions = $computed(() => { ...@@ -26,8 +27,8 @@ const listOptions = $computed(() => {
{ label: '学位', prop: 'degree_name', align: 'center' }, { label: '学位', prop: 'degree_name', align: 'center' },
{ label: '生效状态', slots: 'status', align: 'center' }, { label: '生效状态', slots: 'status', align: 'center' },
{ label: '更新时间', prop: 'updated_time', align: 'center', width: 200 }, { label: '更新时间', prop: 'updated_time', align: 'center', width: 200 },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' } { label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' },
] ],
} }
}) })
const handleAddPro = () => { const handleAddPro = () => {
...@@ -71,6 +72,7 @@ const handleChangeStatus = (row: any) => { ...@@ -71,6 +72,7 @@ const handleChangeStatus = (row: any) => {
<AppCard title="专业管理"> <AppCard title="专业管理">
<AppList v-bind="listOptions" ref="appList" border stripe style="margin-top: 30px"> <AppList v-bind="listOptions" ref="appList" border stripe style="margin-top: 30px">
<el-button type="primary" round @click="handleAddPro">新增专业</el-button> <el-button type="primary" round @click="handleAddPro">新增专业</el-button>
<ImportPro @create="handleFresh" />
<template #status="{ row }"> <template #status="{ row }">
<el-switch <el-switch
size="large" size="large"
...@@ -80,8 +82,7 @@ const handleChangeStatus = (row: any) => { ...@@ -80,8 +82,7 @@ const handleChangeStatus = (row: any) => {
inline-prompt inline-prompt
style="--el-switch-on-color: #aa1941" style="--el-switch-on-color: #aa1941"
@change="handleChangeStatus(row)" @change="handleChangeStatus(row)"
:disabled="!isAdmin" :disabled="!isAdmin"></el-switch>
></el-switch>
</template> </template>
<template #table-operate="{ row }"> <template #table-operate="{ row }">
<el-space> <el-space>
...@@ -97,6 +98,5 @@ const handleChangeStatus = (row: any) => { ...@@ -97,6 +98,5 @@ const handleChangeStatus = (row: any) => {
:id="id" :id="id"
:title="title" :title="title"
:isEdit="isEdit" :isEdit="isEdit"
@create="handleFresh" @create="handleFresh" />
/>
</template> </template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { splitStrLast } from '@/utils/util'
import { addSem } from '../api'
import * as XLSX from 'xlsx'
import { useUserStore } from '@/stores/user'
import { useProjectList } from '@/composables/useGetProjectList'
const emit = defineEmits<Emits>()
const uploadRef = ref()
const fileList = ref([]) // 文件列表
const isShowImportDialog = ref(false)
const tableData = ref<any[]>([]) // 导入的数据
const departmentList: any = useProjectList('', '79806610719731712').departmentList
const userStore = useUserStore()
defineProps({
isShowImportDialog: {
type: Boolean,
},
})
interface Emits {
(e: 'update:isShowImportDialog', isShowImportDialog: boolean): void
(e: 'create'): void
}
// 打开导入弹框
const handleImport = () => {
isShowImportDialog.value = true
tableData.value = []
fileList.value = []
}
// 取消
const handleCancel = () => {
isShowImportDialog.value = false
tableData.value = []
fileList.value = []
}
// 文件上传前校验
const beforeUpload = (file: any) => {
const suffix = splitStrLast(file.name, '.')
if (!['xlsx', 'xls'].includes(suffix)) {
ElMessage.warning('只能上传excel文件')
return false
} else {
return true
}
}
// 读取Excel文件
const readExcel = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e: any) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
resolve(jsonData)
} catch (error) {
reject(error)
}
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
// 解析Excel数据
const parseExcelData = (jsonData: any[]) => {
const result: any[] = []
jsonData.forEach((row: any, index: number) => {
// 根据Excel列名映射数据
const item: any = {
id: index + 1,
name: row['学期名称'] || row['name'] || '',
organ_id: '',
organ_id_name: row['所属部门/学校'] || row['organ_id_name'] || '',
start_time: row['学期开始日期'] || row['start_time'] || '',
end_time: row['学期结束日期'] || row['end_time'] || '',
length: row['教学周'] || row['length'] || '',
status: 'pending', // 初始状态:待导入
errorMsg: '', // 错误信息
}
// 根据部门名称查找部门ID
if (item.organ_id_name) {
const department = departmentList.value?.find((d: any) => d.name === item.organ_id_name)
if (department) {
item.organ_id = department.id
}
}
// 如果没有找到部门ID,使用当前用户的部门ID
if (!item.organ_id && userStore.organization?.id) {
item.organ_id = userStore.organization.id
item.organ_id_name = userStore.organization.name
}
result.push(item)
})
return result
}
// 文件上传处理
const handleFileChange = async (file: any) => {
try {
const jsonData = await readExcel(file.raw)
tableData.value = parseExcelData(jsonData as any[])
ElMessage.success('Excel解析成功')
} catch (error) {
ElMessage.error('Excel解析失败,请检查文件格式')
console.error('Excel解析错误:', error)
}
}
// 批量导入
const handleBatchImport = async () => {
if (tableData.value.length === 0) {
ElMessage.warning('请先上传Excel文件')
return
}
// 验证数据
const invalidData = tableData.value.filter((item) => !item.name || !item.organ_id)
if (invalidData.length > 0) {
ElMessage.warning('存在数据不完整,请检查')
return
}
let successCount = 0
let failCount = 0
// 逐条导入
for (const item of tableData.value) {
if (item.status === 'success') continue // 跳过已成功的
item.status = 'importing'
try {
const params: any = {
name: item.name,
organ_id: item.organ_id,
start_time: item.start_time || undefined,
end_time: item.end_time || undefined,
length: item.length || undefined,
status: '1',
}
await addSem(params)
item.status = 'success'
successCount++
} catch (error: any) {
item.status = 'failed'
item.errorMsg = error.message || '导入失败'
failCount++
}
}
// 显示导入结果
if (failCount === 0) {
ElMessage.success(`成功导入 ${successCount} 条数据`)
emit('create')
setTimeout(() => {
handleCancel()
}, 1500)
} else {
ElMessage.warning(`成功导入 ${successCount} 条,失败 ${failCount} 条`)
}
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: any = {
pending: '待导入',
importing: '导入中',
success: '成功',
failed: '失败',
}
return statusMap[status] || ''
}
// 获取状态类型
const getStatusType = (status: string) => {
const typeMap: any = {
pending: 'info',
importing: 'warning',
success: 'success',
failed: 'danger',
}
return typeMap[status] || 'info'
}
</script>
<template>
<el-button type="primary" round @click="handleImport">批量导入</el-button>
<el-dialog
draggable
v-model="isShowImportDialog"
:close-on-click-modal="false"
:before-close="handleCancel"
title="批量导入学期"
width="900px">
<div>
<el-upload
style="text-align: center"
class="file-import"
ref="uploadRef"
action="#"
accept=".xls,.xlsx"
drag
:auto-upload="false"
:file-list="fileList"
:limit="1"
:before-upload="beforeUpload"
:on-change="handleFileChange">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖至此处,点击上传</div>
</el-upload>
<div style="margin-bottom: 10px; text-align: right">
<a href="/center_resource/学期导入模板.xlsx" download="学期导入模板">
<el-link type="primary">下载模板</el-link>
</a>
</div>
<!-- 数据预览表格 -->
<el-table v-if="tableData.length > 0" :data="tableData" border max-height="400" style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="name" label="学期名称" align="center" />
<el-table-column prop="organ_id_name" label="所属部门/学校" align="center" />
<el-table-column prop="start_time" label="学期开始日期" align="center" />
<el-table-column prop="end_time" label="学期结束日期" align="center" />
<el-table-column prop="length" label="教学周" align="center" />
<el-table-column label="导入状态" align="center" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="错误信息" align="center" width="200">
<template #default="{ row }">
<span style="color: red">{{ row.errorMsg }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<span>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleBatchImport" :disabled="tableData.length === 0"> 开始导入 </el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.file-import {
margin-bottom: 20px;
}
</style>
...@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus' ...@@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import AddSemester from '../components/AddSemester.vue' import AddSemester from '../components/AddSemester.vue'
import SemesterCourse from '../components/SemesterCourse.vue' import SemesterCourse from '../components/SemesterCourse.vue'
import ImportSemester from '../components/ImportSemester.vue'
import { getSemList, updateSem } from '../api' import { getSemList, updateSem } from '../api'
const departmentList: any = useProjectList('', '79806610719731712').departmentList const departmentList: any = useProjectList('', '79806610719731712').departmentList
// 判断当前用户是不是超级管理员 // 判断当前用户是不是超级管理员
...@@ -20,7 +21,7 @@ const listOptions = $computed(() => { ...@@ -20,7 +21,7 @@ const listOptions = $computed(() => {
remote: { httpRequest: getSemList, params: { name: '', organ_id: '' } }, remote: { httpRequest: getSemList, params: { name: '', organ_id: '' } },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '学期名称:', placeholder: '学期名称' }, { type: 'input', prop: 'name', label: '学期名称:', placeholder: '学期名称' },
{ type: 'select', prop: 'organ_id', slots: 'filter-department' } { type: 'select', prop: 'organ_id', slots: 'filter-department' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', align: 'center' }, { label: '序号', type: 'index', align: 'center' },
...@@ -31,8 +32,8 @@ const listOptions = $computed(() => { ...@@ -31,8 +32,8 @@ const listOptions = $computed(() => {
{ label: '教学周', prop: 'length', align: 'center' }, { label: '教学周', prop: 'length', align: 'center' },
{ label: '生效状态', slots: 'status', align: 'center' }, { label: '生效状态', slots: 'status', align: 'center' },
{ label: '更新时间', prop: 'updated_time', align: 'center' }, { label: '更新时间', prop: 'updated_time', align: 'center' },
{ label: '操作', slots: 'table-operate', align: 'center', width: 300, fixed: 'right' } { label: '操作', slots: 'table-operate', align: 'center', width: 300, fixed: 'right' },
] ],
} }
}) })
const handleAddSemester = () => { const handleAddSemester = () => {
...@@ -83,6 +84,7 @@ const handleChangeStatus = (row: any) => { ...@@ -83,6 +84,7 @@ const handleChangeStatus = (row: any) => {
<el-button type="primary" round @click="handleAddSemester" v-permission="'v1-learning-semester-create'" <el-button type="primary" round @click="handleAddSemester" v-permission="'v1-learning-semester-create'"
>新增学期</el-button >新增学期</el-button
> >
<ImportSemester @create="handleFresh" />
<template v-if="isAdmin" #filter-department="{ params }"> <template v-if="isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属部门/学校:</div> <div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属部门/学校:</div>
<el-select @change="handleFresh" clearable v-model="params.organ_id" placeholder="请选择所属部门/学校"> <el-select @change="handleFresh" clearable v-model="params.organ_id" placeholder="请选择所属部门/学校">
...@@ -98,8 +100,7 @@ const handleChangeStatus = (row: any) => { ...@@ -98,8 +100,7 @@ const handleChangeStatus = (row: any) => {
inline-prompt inline-prompt
style="--el-switch-on-color: #aa1941" style="--el-switch-on-color: #aa1941"
@change="handleChangeStatus(row)" @change="handleChangeStatus(row)"
:disabled="!isAdmin" :disabled="!isAdmin"></el-switch>
></el-switch>
</template> </template>
<template #table-operate="{ row }"> <template #table-operate="{ row }">
<el-space> <el-space>
...@@ -122,11 +123,9 @@ const handleChangeStatus = (row: any) => { ...@@ -122,11 +123,9 @@ const handleChangeStatus = (row: any) => {
:title="title" :title="title"
:id="id" :id="id"
:isEdit="isEdit" :isEdit="isEdit"
@create="handleFresh" @create="handleFresh" />
/>
<SemesterCourse <SemesterCourse
v-model:isShowSemCourseDialog="isShowSemCourseDialog" v-model:isShowSemCourseDialog="isShowSemCourseDialog"
v-if="isShowSemCourseDialog === true" v-if="isShowSemCourseDialog === true"
:id="id" :id="id" />
/>
</template> </template>
<script lang="ts" setup>
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { splitStrLast } from '@/utils/util'
import { addStaff } from '../api'
import * as XLSX from 'xlsx'
import { useUserStore } from '@/stores/user'
import { useProjectList } from '@/composables/useGetProjectList'
import { useMapStore } from '@/stores/map'
const emit = defineEmits<Emits>()
const uploadRef = ref()
const fileList = ref([]) // 文件列表
const isShowImportDialog = ref(false)
const tableData = ref<any[]>([]) // 导入的数据
const store = useMapStore()
const userStore = useUserStore()
defineProps({
isShowImportDialog: {
type: Boolean,
},
})
interface Emits {
(e: 'update:isShowImportDialog', isShowImportDialog: boolean): void
(e: 'create'): void
}
// 获取字典数据
const departmentList: any = useProjectList('', '79806610719731712').departmentList
const sexList = ref<any[]>([])
const roleList = ref<any[]>([])
// 打开导入弹框
const handleImport = () => {
isShowImportDialog.value = true
tableData.value = []
fileList.value = []
// 获取字典数据
sexList.value = store.getMapValuesByKey('system_gender')
roleList.value = store.getMapValuesByKey('teacher_role')
}
// 取消
const handleCancel = () => {
isShowImportDialog.value = false
tableData.value = []
fileList.value = []
}
// 文件上传前校验
const beforeUpload = (file: any) => {
const suffix = splitStrLast(file.name, '.')
if (!['xlsx', 'xls'].includes(suffix)) {
ElMessage.warning('只能上传excel文件')
return false
} else {
return true
}
}
// 读取Excel文件
const readExcel = (file: File) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e: any) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
resolve(jsonData)
} catch (error) {
reject(error)
}
}
reader.onerror = reject
reader.readAsArrayBuffer(file)
})
}
// 根据字典label查找value
const findValueByLabel = (list: any[], label: string) => {
const item = list.find((item: any) => item.label === label)
return item ? item.value : ''
}
// 解析Excel数据
const parseExcelData = (jsonData: any[]) => {
const result: any[] = []
jsonData.forEach((row: any, index: number) => {
// 根据Excel列名映射数据
const item: any = {
id: index + 1,
organ_id: '',
organ_id_name: row['所属部门/学校'] || row['organ_id_name'] || '',
name: row['姓名'] || row['name'] || '',
gender: '',
gender_name: row['性别'] || row['gender_name'] || '',
mobile: row['手机号'] || row['mobile'] || '',
role: '',
role_name: row['角色类型'] || row['role_name'] || '',
email: row['邮箱'] || row['email'] || '',
status: 'pending', // 初始状态:待导入
errorMsg: '', // 错误信息
}
// 根据部门名称查找部门ID
if (item.organ_id_name) {
const department = departmentList.value?.find((d: any) => d.name === item.organ_id_name)
if (department) {
item.organ_id = department.id
}
}
// 如果没有找到部门ID,使用当前用户的部门ID
if (!item.organ_id && userStore.organization?.id) {
item.organ_id = userStore.organization.id
item.organ_id_name = userStore.organization.name
}
// 根据性别名称查找value
if (item.gender_name) {
item.gender = findValueByLabel(sexList.value, item.gender_name)
}
// 根据角色名称查找value(支持多个角色,用逗号分隔)
if (item.role_name) {
const roleNames = item.role_name.split(',').map((r: string) => r.trim())
const roleValues = roleNames
.map((name: string) => findValueByLabel(roleList.value, name))
.filter((v: string) => v !== '')
item.role = roleValues.join(',')
}
result.push(item)
})
return result
}
// 文件上传处理
const handleFileChange = async (file: any) => {
try {
const jsonData = await readExcel(file.raw)
tableData.value = parseExcelData(jsonData as any[])
ElMessage.success('Excel解析成功')
} catch (error) {
ElMessage.error('Excel解析失败,请检查文件格式')
console.error('Excel解析错误:', error)
}
}
// 批量导入
const handleBatchImport = async () => {
if (tableData.value.length === 0) {
ElMessage.warning('请先上传Excel文件')
return
}
// 验证数据
const invalidData = tableData.value.filter(
(item) => !item.name || !item.organ_id || !item.gender || !item.mobile || !item.role || !item.email
)
if (invalidData.length > 0) {
ElMessage.warning('存在数据不完整,请检查')
return
}
let successCount = 0
let failCount = 0
// 逐条导入
for (const item of tableData.value) {
if (item.status === 'success') continue // 跳过已成功的
item.status = 'importing'
try {
const params: any = {
name: item.name,
organ_id: item.organ_id,
gender: item.gender,
mobile: item.mobile,
role: item.role,
email: item.email,
status: '1',
}
await addStaff(params)
item.status = 'success'
successCount++
} catch (error: any) {
item.status = 'failed'
item.errorMsg = error.message || '导入失败'
failCount++
}
}
// 显示导入结果
if (failCount === 0) {
ElMessage.success(`成功导入 ${successCount} 条数据`)
emit('create')
setTimeout(() => {
handleCancel()
}, 1500)
} else {
ElMessage.warning(`成功导入 ${successCount} 条,失败 ${failCount} 条`)
}
}
// 获取状态文本
const getStatusText = (status: string) => {
const statusMap: any = {
pending: '待导入',
importing: '导入中',
success: '成功',
failed: '失败',
}
return statusMap[status] || ''
}
// 获取状态类型
const getStatusType = (status: string) => {
const typeMap: any = {
pending: 'info',
importing: 'warning',
success: 'success',
failed: 'danger',
}
return typeMap[status] || 'info'
}
</script>
<template>
<el-button type="primary" round @click="handleImport">批量导入</el-button>
<el-dialog
draggable
v-model="isShowImportDialog"
:close-on-click-modal="false"
:before-close="handleCancel"
title="批量导入教工"
width="1000px">
<div>
<el-upload
style="text-align: center"
class="file-import"
ref="uploadRef"
action="#"
accept=".xls,.xlsx"
drag
:auto-upload="false"
:file-list="fileList"
:limit="1"
:before-upload="beforeUpload"
:on-change="handleFileChange">
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">将文件拖至此处,点击上传</div>
</el-upload>
<div style="margin-bottom: 10px; text-align: right">
<a href="/center_resource/教工导入模板.xlsx" download="教工导入模板">
<el-link type="primary">下载模板</el-link>
</a>
</div>
<!-- 数据预览表格 -->
<el-table v-if="tableData.length > 0" :data="tableData" border max-height="400" style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="organ_id_name" label="所属部门/学校" align="center" />
<el-table-column prop="name" label="姓名" align="center" />
<el-table-column prop="gender_name" label="性别" align="center" />
<el-table-column prop="mobile" label="手机号" align="center" />
<el-table-column prop="role_name" label="角色类型" align="center" />
<el-table-column prop="email" label="邮箱" align="center" />
<el-table-column label="导入状态" align="center" width="120">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="错误信息" align="center" width="200">
<template #default="{ row }">
<span style="color: red">{{ row.errorMsg }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<span>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleBatchImport" :disabled="tableData.length === 0"> 开始导入 </el-button>
</span>
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.file-import {
margin-bottom: 20px;
}
</style>
<script setup lang="ts"> <script setup lang="ts">
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import AddStaff from '../components/AddStaff.vue' import AddStaff from '../components/AddStaff.vue'
import ImportStaff from '../components/ImportStaff.vue'
import { useProjectList } from '@/composables/useGetProjectList' import { useProjectList } from '@/composables/useGetProjectList'
import { getStaffList, updateStaff } from '../api' import { getStaffList, updateStaff } from '../api'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
...@@ -23,12 +24,12 @@ const listOptions = $computed(() => { ...@@ -23,12 +24,12 @@ const listOptions = $computed(() => {
httpRequest: getStaffList, httpRequest: getStaffList,
params: { params: {
name: '', name: '',
organ_id: '' organ_id: '',
} },
}, },
filters: [ filters: [
{ type: 'input', prop: 'name', label: '姓名:', placeholder: '姓名' }, { type: 'input', prop: 'name', label: '姓名:', placeholder: '姓名' },
{ type: 'select', prop: 'organ_id', slots: 'filter-department' } { type: 'select', prop: 'organ_id', slots: 'filter-department' },
], ],
columns: [ columns: [
{ label: '序号', type: 'index', align: 'center' }, { label: '序号', type: 'index', align: 'center' },
...@@ -40,8 +41,8 @@ const listOptions = $computed(() => { ...@@ -40,8 +41,8 @@ const listOptions = $computed(() => {
{ label: '角色类型', prop: 'role_name', align: 'center' }, { label: '角色类型', prop: 'role_name', align: 'center' },
{ label: '生效状态', slots: 'status', align: 'center' }, { label: '生效状态', slots: 'status', align: 'center' },
{ label: '更新时间', prop: 'updated_time', align: 'center', width: 200 }, { label: '更新时间', prop: 'updated_time', align: 'center', width: 200 },
{ label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' } { label: '操作', slots: 'table-operate', align: 'center', width: 200, fixed: 'right' },
] ],
} }
}) })
...@@ -93,6 +94,7 @@ const handleChangeStatus = (row: any) => { ...@@ -93,6 +94,7 @@ const handleChangeStatus = (row: any) => {
<el-button type="primary" round @click="handleAddStudent" v-permission="'v1-learning-teacher-create'" <el-button type="primary" round @click="handleAddStudent" v-permission="'v1-learning-teacher-create'"
>新增教工</el-button >新增教工</el-button
> >
<ImportStaff @create="handleFresh" />
<template v-if="isAdmin" #filter-department="{ params }"> <template v-if="isAdmin" #filter-department="{ params }">
<div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属部门/学校:</div> <div class="name" style="font-size: 14px; color: #606266; padding-right: 12px">所属部门/学校:</div>
<el-select @change="handleFresh" clearable v-model="params.organ_id" placeholder="请选择所属部门/学校"> <el-select @change="handleFresh" clearable v-model="params.organ_id" placeholder="请选择所属部门/学校">
...@@ -108,8 +110,7 @@ const handleChangeStatus = (row: any) => { ...@@ -108,8 +110,7 @@ const handleChangeStatus = (row: any) => {
inline-prompt inline-prompt
style="--el-switch-on-color: #aa1941" style="--el-switch-on-color: #aa1941"
@change="handleChangeStatus(row)" @change="handleChangeStatus(row)"
:disabled="!isAdmin" :disabled="!isAdmin"></el-switch>
></el-switch>
</template> </template>
<template #table-operate="{ row }"> <template #table-operate="{ row }">
<el-space> <el-space>
...@@ -129,6 +130,5 @@ const handleChangeStatus = (row: any) => { ...@@ -129,6 +130,5 @@ const handleChangeStatus = (row: any) => {
:title="title" :title="title"
:isEdit="isEdit" :isEdit="isEdit"
:id="id" :id="id"
@create="handleFresh" @create="handleFresh" />
/>
</template> </template>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论