feat: 完成学生管理页面

frontend/dev
Spark 2 months ago
parent ee87486fa8
commit 884c22c0f4

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@ -0,0 +1,20 @@
@font-face {
font-family: 'Nautilus-pompilius';
src: url('@/assets/fonts/Nautilus-pompilius-2.woff2') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'YaYa点名';
src: url('@/assets/fonts/YaYa点名.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: '隶书-online';
src: url('@/assets/fonts/隶书.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}

@ -0,0 +1,27 @@
@font-face {
font-family: "iconfont"; /* Project id 4702540 */
src: url('iconfont.woff2?t=1728542908069') format('woff2'),
url('iconfont.woff?t=1728542908069') format('woff'),
url('iconfont.ttf?t=1728542908069') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.yaya-xmark:before {
content: "\e649";
}
.yaya-duigou:before {
content: "\e60b";
}
.yaya-download:before {
content: "\e600";
}

@ -0,0 +1 @@
window._iconfont_svg_string_4702540='<svg><symbol id="yaya-xmark" viewBox="0 0 1024 1024"><path d="M813.2 301.2c25-25 25-65.6 0-90.6s-65.6-25-90.6 0L512 421.4 301.2 210.8c-25-25-65.6-25-90.6 0s-25 65.6 0 90.6L421.4 512 210.8 722.8c-25 25-25 65.6 0 90.6s65.6 25 90.6 0L512 602.6l210.8 210.6c25 25 65.6 25 90.6 0s25-65.6 0-90.6L602.6 512l210.6-210.8z" ></path></symbol><symbol id="yaya-duigou" viewBox="0 0 1024 1024"><path d="M964.693333 229.333333a55.466667 55.466667 0 0 1 0 78.08L472.106667 794.666667a56.533333 56.533333 0 0 1-33.92 16h-7.466667a55.253333 55.253333 0 0 1-37.546667-16.213334L59.093333 464.853333a55.253333 55.253333 0 0 1 0-77.866666 55.893333 55.893333 0 0 1 78.933334 0l294.4 289.706666 453.333333-448a56.32 56.32 0 0 1 78.933333 0.64z" fill="#7F7F7F" ></path></symbol><symbol id="yaya-download" viewBox="0 0 1024 1024"><path d="M576 64c0-35.4-28.6-64-64-64s-64 28.6-64 64v485.4l-146.8-146.8c-25-25-65.6-25-90.6 0s-25 65.6 0 90.6l256 256c25 25 65.6 25 90.6 0l256-256c25-25 25-65.6 0-90.6s-65.6-25-90.6 0L576 549.4V64zM128 704c-70.6 0-128 57.4-128 128v64c0 70.6 57.4 128 128 128h768c70.6 0 128-57.4 128-128v-64c0-70.6-57.4-128-128-128H693l-90.6 90.6c-50 50-131 50-181 0L331 704H128z m736 112a48 48 0 1 1 0 96 48 48 0 1 1 0-96z" ></path></symbol></svg>',(n=>{var t=(e=(e=document.getElementsByTagName("script"))[e.length-1]).getAttribute("data-injectcss"),e=e.getAttribute("data-disable-injectsvg");if(!e){var o,i,a,c,s,d=function(t,e){e.parentNode.insertBefore(t,e)};if(t&&!n.__iconfont__svg__cssinject__){n.__iconfont__svg__cssinject__=!0;try{document.write("<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>")}catch(t){console&&console.log(t)}}o=function(){var t,e=document.createElement("div");e.innerHTML=n._iconfont_svg_string_4702540,(e=e.getElementsByTagName("svg")[0])&&(e.setAttribute("aria-hidden","true"),e.style.position="absolute",e.style.width=0,e.style.height=0,e.style.overflow="hidden",e=e,(t=document.body).firstChild?d(e,t.firstChild):t.appendChild(e))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(i=function(){document.removeEventListener("DOMContentLoaded",i,!1),o()},document.addEventListener("DOMContentLoaded",i,!1)):document.attachEvent&&(a=o,c=n.document,s=!1,r(),c.onreadystatechange=function(){"complete"==c.readyState&&(c.onreadystatechange=null,l())})}function l(){s||(s=!0,a())}function r(){try{c.documentElement.doScroll("left")}catch(t){return void setTimeout(r,50)}l()}})(window);

@ -0,0 +1,30 @@
{
"id": "4702540",
"name": "YaYa",
"font_family": "iconfont",
"css_prefix_text": "yaya-",
"description": "",
"glyphs": [
{
"icon_id": "35012394",
"name": "xmark",
"font_class": "xmark",
"unicode": "e649",
"unicode_decimal": 58953
},
{
"icon_id": "21163946",
"name": "对勾",
"font_class": "duigou",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "35007441",
"name": "download",
"font_class": "download",
"unicode": "e600",
"unicode_decimal": 58880
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,32 @@
body {
margin: 0;
background-color: #f5f5f5;
}
/* fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
transition: all 0.3s;
}
.fade-slide-enter-from {
transform: translateX(-30px);
opacity: 0;
}
.fade-slide-leave-to {
transform: translateX(30px);
opacity: 0;
}
html {
height: 100%;
}
#app {
min-height: 100%;
}
* {
box-sizing: border-box;
}

@ -0,0 +1,765 @@
<script setup lang="ts">
import {onMounted, ref} from 'vue'
import {CirclePlusFilled, Delete, Edit, RefreshLeft, Search, UploadFilled} from "@element-plus/icons-vue"
import {deleteStudents, getStudentById, pageStudent, saveStudent, updateStudent, upload, download} from '@/api/student.js'
import {ElMessage, ElMessageBox} from "element-plus"
const tableData = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const loading = ref(false)
const loadingEditDialog = ref(false)
const loadingSaveDialog = ref(false)
const loadingUpload = ref(false)
const loadingDownload = ref(false)
const addStudentDialogVisible = ref(false)
const editStudentDialogVisible = ref(false)
const ruleFormRef = ref()
const formSize = ref('default')
const pageQueryForm = ref({
no: '',
name: '',
gender: '',
className: '',
exactQuery: false,
createTime: '',
updateTime: '',
orderBy: '',
orderRule: ''
})
const studentForm = ref({
id: '',
no: '',
name: '',
gender: '',
className: '',
points: ''
})
const multipleSelection = ref([])
const handlePageQuery = async ({prop = null, order = null}) => {
loading.value = true
if (prop && order) {
if (prop !== 'Empty' && order !== 'Empty') {
pageQueryForm.value.orderBy = prop
pageQueryForm.value.orderRule = (order === 'ascending' ? 'ASC' : 'DESC')
}
} else {
pageQueryForm.value.orderBy = ''
pageQueryForm.value.orderRule = ''
}
const queryParams = {
...pageQueryForm.value,
page: currentPage.value,
pageSize: pageSize.value
}
const response = await pageStudent(queryParams)
if (response.code === 1) {
tableData.value = response.data.records
total.value = response.data.total
// ElMessage.success('')
} else {
ElMessage.error('查询失败,原因: ' + response.msg)
}
loading.value = false
}
onMounted(() => {
handlePageQuery({prop: 'Empty', order: 'Empty'})
})
const resetQueryForm = () => {
pageQueryForm.value = {
no: '',
name: '',
gender: '',
major: '',
className: '',
exactQuery: false,
createTime: '',
updateTime: ''
}
currentPage.value = 1
handlePageQuery({prop: 'Empty', order: 'Empty'})
}
const resetStudentForm = () => {
studentForm.value = {
no: '',
name: '',
gender: '',
major: '',
className: '',
points: ''
}
}
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
const handleDelete = (row) => {
ElMessageBox.confirm(
'删除不可撤销,继续吗?',
'警告',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const response = await deleteStudents(row.id)
if (response.code == 1) {
ElMessage.success('删除成功')
await handlePageQuery({prop: 'Empty', order: 'Empty'})
} else {
ElMessage.error('删除失败')
}
}).catch(() => {
ElMessage.info('删除取消')
})
}
const handleBatchDelete = () => {
ElMessageBox.confirm(
'删除不可撤销,继续吗?',
'警告',
{
confirmButtonText: '继续',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const ids = multipleSelection.value.map(item => item.id)
const response = await deleteStudents(ids)
if (response.code === 1) {
ElMessage.success('删除成功')
await handlePageQuery({prop: 'Empty', order: 'Empty'})
} else {
ElMessage.error('删除失败')
}
}).catch(() => {
ElMessage.info('删除取消')
})
}
const handleAddStudent = async () => {
if (!ruleFormRef.value) return
await ruleFormRef.value.validate(async (valid, fields) => {
if (valid) {
loadingSaveDialog.value = true
const response = await saveStudent(studentForm.value)
if (response.code === 1) {
ElMessage.success('保存成功')
addStudentDialogVisible.value = false
await handlePageQuery({prop: 'Empty', order: 'Empty'})
} else {
ElMessage.error(response.msg || '保存失败')
}
loadingSaveDialog.value = false
} else {
return false
}
})
}
const handleEditStudent = async (row) => {
loadingEditDialog.value = true
resetStudentForm()
editStudentDialogVisible.value = true
const response = await getStudentById(row.id)
// console.log(response.data.className)
studentForm.value = {
...response.data,
className: response.data.className.toString(),
points: response.data.points.toString()
}
loadingEditDialog.value = false
}
const handleCommitStudent = async () => {
if (!ruleFormRef.value) return
await ruleFormRef.value.validate(async (valid, fields) => {
if (valid) {
loadingSaveDialog.value = true
const response = await updateStudent(studentForm.value)
if (response.code === 1) {
ElMessage.success('保存成功')
editStudentDialogVisible.value = false
await handlePageQuery({prop: 'Empty', order: 'Empty'})
} else {
ElMessage.error(response.msg || '保存失败')
}
loadingSaveDialog.value = false
} else {
return false
}
})
}
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
handlePageQuery({prop: 'Empty', order: 'Empty'})
}
const handleCurrentChange = (val) => {
currentPage.value = val
handlePageQuery({prop: 'Empty', order: 'Empty'})
}
const rules = {
no: [
{required: true, message: '请输入学号', trigger: 'change'},
{max: 20, message: '最大长度应为 20 个字符', trigger: 'change'}
],
name: [
{required: true, message: '请输入姓名', trigger: 'change'},
{max: 20, message: '最大长度为 20 个字符', trigger: 'change'}
],
gender: [
{required: true, message: '请输入选择性别', trigger: 'change'}
],
className: [
{required: false, message: '请输入班级', trigger: 'change'},
{max: 20, message: '最大长度为 20 个字符', trigger: 'change'}
],
points: [
{max: 16, message: '最大为 16 位', trigger: 'change'}
]
}
const formatGender = (row) => {
return row.gender === 0 ? '男' : '女'
}
const gender_options = [
{
value: '',
label: '全部'
},
{
value: '0',
label: '男'
},
{
value: '1',
label: '女'
}
]
const shortcuts = [
{
text: '近一周',
value: () => {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 7)
return [start, end]
},
},
{
text: '近一月',
value: () => {
const end = new Date()
const start = new Date()
start.setMonth(start.getMonth() - 1)
return [start, end]
},
},
{
text: '近三月',
value: () => {
const end = new Date()
const start = new Date()
start.setMonth(start.getMonth() - 3)
return [start, end]
},
},
]
const beforeUpload = (file) => {
const isExcel = file.type === 'application/vnd.ms-excel' ||
file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
if (!isExcel) {
ElMessage.error('只能上传 Excel 文件!')
return false
}
return true
}
const handleUpload = async ({file}) => {
loadingUpload.value = true
const formData = new FormData()
formData.append('file', file)
const response = await upload(formData)
if (response.code === 1) {
ElMessage.success('导入成功')
await handlePageQuery({prop: 'Empty', order: 'Empty'})
} else {
ElMessage.error('导入失败,原因: ' + response.msg)
}
loadingUpload.value = false
}
const handleDownload = async () => {
try {
loadingDownload.value = true
const response = await download();
const blob = new Blob([response.data], { type: response.data.type });
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
downloadLink.download = 'students.xlsx';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
ElMessage.success('导出成功');
} catch (error) {
console.error('Download failed:', error);
ElMessage.error('导出失败');
}
loadingDownload.value = false
}
</script>
<template>
<el-card v-loading="loading">
<div class="query-form">
<el-form :model="pageQueryForm" label-width="80px" class="filter-form">
<!-- 第一行学号姓名性别 -->
<div class="form-row">
<el-form-item label="学号">
<el-input
v-model="pageQueryForm.no"
placeholder="请输入学号"
clearable
class="custom-input"
/>
</el-form-item>
<el-form-item label="姓名">
<el-input
v-model="pageQueryForm.name"
placeholder="请输入姓名"
clearable
class="custom-input"
/>
</el-form-item>
<el-form-item label="性别">
<el-select
v-model="pageQueryForm.gender"
placeholder="请选择性别"
clearable
class="custom-input"
>
<el-option
v-for="item in gender_options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</div>
<!-- 第二行时间 -->
<div class="form-row">
<el-form-item label="创建时间" class="date-item">
<el-date-picker
v-model="pageQueryForm.createTime"
type="datetimerange"
:shortcuts="shortcuts"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
class="date-picker"
/>
</el-form-item>
<el-form-item label="最后操作" class="date-item">
<el-date-picker
v-model="pageQueryForm.updateTime"
type="datetimerange"
:shortcuts="shortcuts"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
class="date-picker"
/>
</el-form-item>
</div>
<!-- 第三行按钮 -->
<div class="form-row">
<div class="button-wrapper">
<el-button type="primary" :icon="Search" @click="handlePageQuery({prop: 'Empty', order: 'Empty'})">查询</el-button>
<el-button
@click="resetQueryForm"
:icon="RefreshLeft"
:disabled="pageQueryForm.no === '' && pageQueryForm.name === '' &&
pageQueryForm.className === '' && pageQueryForm.exactQuery === false && pageQueryForm.gender === '' &&
pageQueryForm.updateTime === '' && pageQueryForm.createTime === ''"
>
重置
</el-button>
</div>
</div>
</el-form>
</div>
<div class="table-toolbar">
<el-button type="danger" :icon="Delete" @click="handleBatchDelete" :disabled="!multipleSelection.length">
批量删除
</el-button>
<el-button type="primary" :icon="CirclePlusFilled" @click="resetStudentForm(); addStudentDialogVisible = true">
新增学生
</el-button>
<el-upload
:auto-upload="true"
:show-file-list="false"
:http-request="handleUpload"
:before-upload="beforeUpload"
accept=".xlsx,.xls"
>
<el-button type="primary" :icon="UploadFilled" style="margin-left: 12px" :loading="loadingUpload">
</el-button>
</el-upload>
<el-button type="primary" style="margin-left: 12px" @click="handleDownload" :loading="loadingDownload">
<i class="iconfont yaya-download" style="font-size: 10px"></i>
&nbsp;&nbsp;
</el-button>
</div>
<el-table
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
@sort-change="handlePageQuery"
border
stripe
class="student-table"
>
<el-table-column type="selection" width="40"/>
<el-table-column prop="no" label="学号" sortable="custom" width="120" show-overflow-tooltip/>
<el-table-column prop="name" label="姓名" sortable="custom" width="120" show-overflow-tooltip/>
<el-table-column prop="gender" label="性别" :formatter="formatGender" sortable="custom"/>
<el-table-column prop="className" label="班级" sortable="custom" width="120" show-overflow-tooltip/>
<el-table-column prop="points" label="积分" sortable="custom" show-overflow-tooltip/>
<el-table-column prop="createTime" label="创建时间" sortable="custom" width="180"/>
<el-table-column prop="updateTime" label="最后操作时间" sortable="custom" width="180"/>
<el-table-column label="操作" width="120" fixed="right">
<template #default="scope">
<el-button type="primary" :icon="Edit" circle title="编辑" @click="handleEditStudent(scope.row)"/>
<el-button type="danger" :icon="Delete" circle @click="handleDelete(scope.row)" title="删除"/>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<el-dialog
v-model="addStudentDialogVisible"
title="新增学生"
width="460px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form
ref="ruleFormRef"
:model="studentForm"
:rules="rules"
label-position="right"
label-width="80px"
:size="formSize"
status-icon
class="add-student-form"
v-loading="loadingSaveDialog"
element-loading-text="保存中..."
>
<el-form-item label="学号" prop="no">
<el-input
v-model.trim="studentForm.no"
placeholder="请输入学号"
/>
</el-form-item>
<el-form-item label="姓名" prop="name">
<el-input
v-model.trim="studentForm.name"
placeholder="请输入姓名"
/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="studentForm.gender">
<el-radio-button :value="0"></el-radio-button>
<el-radio-button :value="1"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="班级" prop="className">
<el-input
v-model.trim="studentForm.className"
placeholder="请输入班级"
/>
</el-form-item>
<el-form-item label="积分" prop="points">
<el-input
v-model.trim="studentForm.points"
placeholder="请输入积分,默认为 0"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="addStudentDialogVisible = false"> </el-button>
<el-button type="primary" @click="handleAddStudent">
</el-button>
</div>
</template>
</el-dialog>
<!-- 编辑学生信息-->
<el-dialog
v-model="editStudentDialogVisible"
title="编辑学生"
width="460px"
:close-on-click-modal="false"
destroy-on-close
>
<el-form
ref="ruleFormRef"
:model="studentForm"
:rules="rules"
label-position="right"
label-width="80px"
:size="formSize"
status-icon
v-loading="loadingEditDialog"
element-loading-text="加载中..."
class="edit-student-form"
>
<el-form-item label="学 号" prop="no">
<el-input
v-model.trim="studentForm.no"
placeholder="请输入学号"
/>
</el-form-item>
<el-form-item label="姓 名" prop="name">
<el-input
v-model.trim="studentForm.name"
placeholder="请输入姓名"
/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="studentForm.gender">
<el-radio-button :value="0"></el-radio-button>
<el-radio-button :value="1"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="班级" prop="className">
<el-input
v-model.trim="studentForm.className"
placeholder="请输入班级"
/>
</el-form-item>
<el-form-item label="积分" prop="points">
<el-input
v-model.trim="studentForm.points"
placeholder="请输入积分,默认为 0"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editStudentDialogVisible = false"> </el-button>
<el-button type="primary" @click="handleCommitStudent" :loading="loadingSaveDialog">
</el-button>
</div>
</template>
</el-dialog>
</template>
<style scoped>
.query-form {
padding: 16px;
}
.filter-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-row {
display: flex;
gap: 20px;
align-items: flex-start;
flex-wrap: wrap;
}
.form-row :deep(.el-form-item) {
margin-bottom: 0;
flex: 1;
}
.button-wrapper {
width: 100%;
display: flex;
justify-content: flex-end;
}
.date-item {
flex: 1;
}
.custom-input {
width: 100% !important;
}
.date-picker {
width: 100% !important;
}
:deep(.el-form-item__label) {
font-weight: bold;
color: #606266;
}
:deep(.el-input__wrapper),
:deep(.el-select__wrapper) {
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__wrapper:hover),
:deep(.el-select__wrapper:hover) {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
/* 调整日期选择器的响应式布局 */
@media screen and (max-width: 1400px) {
.date-item {
flex: 0 0 calc(50% - 10px);
}
.date-picker {
width: 100% !important;
}
}
.form-buttons {
margin-left: auto;
}
.student-table :deep(th) {
text-align: center !important;
}
.student-table :deep(.cell) {
display: flex;
justify-content: center;
align-items: center;
}
.table-toolbar {
margin: 16px 0;
display: flex;
gap: 8px;
}
.pagination-container {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
:deep(.el-card__body) {
padding: 20px;
}
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 0;
}
:deep(.el-table) {
margin-top: 8px;
}
.el-dialog {
border-radius: 8px;
}
.el-dialog__body {
padding: 20px 40px;
}
.el-form-item {
margin-bottom: 24px;
}
.el-form-item:last-child {
margin-bottom: 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
.dialog-footer .el-button {
min-width: 100px;
margin-left: 12px;
}
.el-form-item :deep(.el-form-item__content) {
justify-content: flex-start;
}
/* 调整输入框宽度 */
.el-input {
width: 100%;
}
/* 确保单选按钮组对齐 */
.el-radio-group {
width: 100%;
}
.edit-student-form {
min-height: 300px;
}
.add-student-form {
min-height: 300px;
}
</style>
Loading…
Cancel
Save