合并页面,通过路由传递参数实现跳转

master
赵亚鹏 2 weeks ago
commit 77b0180790

36
.gitignore vendored

@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

@ -0,0 +1,38 @@
# my-vue-app
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node", // 使 Node
"baseUrl": ".", //
"paths": {
"@/*": ["src/*"] // @ src
},
"esModuleInterop": true //
},
"include": ["src/**/*"], // src
"exclude": ["node_modules"]
}

3554
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
{
"name": "my-vue-app",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.13.2",
"element-plus": "^2.11.7",
"less": "^4.4.2",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^30.0.0",
"vue": "^3.5.22",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.11",
"vite-plugin-vue-devtools": "^8.0.3"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,18 @@
<script setup>
</script>
<template>
<RouterView></RouterView>
</template>
<style>
#app{
width:100%;
height:100%;
overflow: hidden;
}
</style>

@ -0,0 +1,26 @@
import request from '@/utils/request'
// 获取年级下的班级列表
export const getClassList = (params) => {
return request.get('/class/list', { params })
}
// 新增班级
export const addClass = (data) => {
return request.post('/class/add', data)
}
// 编辑班级
export const editClass = (data) => {
return request.put('/class/edit', data)
}
// 删除班级
export const deleteClass = (id) => {
return request.delete(`/class/delete/${id}`)
}
// 检查班级是否有学生
export const checkClassHasStudent = (classId) => {
return request.get('/class/check', { params: { classId } })
}

@ -0,0 +1,21 @@
import request from '@/utils/request'
// 获取学生列表(支持班级筛选)
export const getStudentList = (params) => {
return request.get('/student/list', { params })
}
// 新增学生
export const addStudent = (data) => {
return request.post('/student/add', data)
}
// 编辑学生
export const editStudent = (data) => {
return request.put('/student/edit', data)
}
// 删除学生
export const deleteStudent = (id) => {
return request.delete(`/student/delete/${id}`)
}

@ -0,0 +1,56 @@
<template>
<el-menu
default-active="/student/grade1"
router
background-color="#545c64"
text-color="white"
active-text-color="#ffd04b"
>
<!-- 学生管理分组 -->
<el-sub-menu index="student">
<template #title>
<el-icon><User /></el-icon>
<span>学生管理</span>
</template>
<el-menu-item index="/student/grade1">一年级</el-menu-item>
<el-menu-item index="/student/grade2">二年级</el-menu-item>
<el-menu-item index="/student/grade3">三年级</el-menu-item>
</el-sub-menu>
<!-- 班级管理分组 -->
<el-sub-menu index="class">
<template #title>
<el-icon><School /></el-icon>
<span>班级管理</span>
</template>
<el-menu-item index="/class/grade1">一年级班级</el-menu-item>
<el-menu-item index="/class/grade2">二年级班级</el-menu-item>
<el-menu-item index="/class/grade3">三年级班级</el-menu-item>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { User, School } from '@element-plus/icons-vue'
</script>
<style lang="less" scoped>
.icons{
width:18px;
height:18px;
margin-right:5px;
}
.el-menu{
border-right:none;
h3{
line-height: 48px;
color:#fff;
text-align: center;
}
}
.el-aside{
height:100%;
background-color: #545c64;
}
</style>

@ -0,0 +1,18 @@
import { createApp } from 'vue'
import App from './App.vue'
import "@/assets/less/index.less"
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
// 1. 先注册 Element Plus
app.use(ElementPlus)
// 2. 再注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 3. 最后挂载应用和路由
app.use(router).mount('#app')

@ -0,0 +1,63 @@
import { createRouter, createWebHistory } from 'vue-router';
import Main from '@/views/Main.vue';
import ClassManagement from '@/views/class/ClassGrade.vue';
import StudentManagement from '@/views/student/StudentManagement.vue';
const routes = [
{
path: '/',
component: Main,
children: [
// 班级管理路由
{
path: 'class/grade1',
name: 'ClassGrade1',
component: ClassManagement,
meta: { title: '一年级班级', grade: 1 }
},
{
path: 'class/grade2',
name: 'ClassGrade2',
component: ClassManagement,
meta: { title: '二年级班级', grade: 2 }
},
{
path: 'class/grade3',
name: 'ClassGrade3',
component: ClassManagement,
meta: { title: '三年级班级', grade: 3 }
},
// 学生管理路由
{
path: 'student/grade1',
name: 'StudentGrade1',
component: StudentManagement,
meta: { title: '一年级学生', grade: 1 }
},
{
path: 'student/grade2',
name: 'StudentGrade2',
component: StudentManagement,
meta: { title: '二年级学生', grade: 2 }
},
{
path: 'student/grade3',
name: 'StudentGrade3',
component: StudentManagement,
meta: { title: '三年级学生', grade: 3 }
}
]
},
{
path: '/',
redirect: '/class/grade1'
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;

@ -0,0 +1,22 @@
import axios from 'axios'
const request = axios.create({
baseURL: '/api', // 后端接口基础路径
timeout: 5000
})
// 请求拦截器
request.interceptors.request.use(config => {
return config
})
// 响应拦截器(处理错误)
request.interceptors.response.use(
response => response.data,
error => {
console.error('请求错误', error)
return Promise.reject(error)
}
)
export default request

@ -0,0 +1,40 @@
<template>
<el-container class="layout-container">
<!-- 左侧导航 -->
<el-aside width="200px" class="aside">
<CommonAside />
</el-aside>
<!-- 右侧内容区 -->
<el-container>
<el-header class="header">
<h1>{{ $route.meta.title || '学生班级管理系统' }}</h1>
</el-header>
<el-main class="main">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import CommonAside from '@/components/CommonAside.vue'
</script>
<style scoped>
.layout-container {
height: 100vh;
}
.aside {
background-color: #545c64;
color: white;
}
.header {
background-color: #f5f5f5;
padding: 0 20px;
}
.main {
padding: 20px;
overflow-y: auto;
}
</style>

@ -0,0 +1,220 @@
<template>
<div class="class-page">
<h2>{{ currentTitle }}</h2>
<!-- 搜索区 + 批量删除按钮 -->
<div class="operate-bar">
<el-input
v-model="searchName"
placeholder="搜索班级名称"
style="width: 300px"
clearable
@clear="loadClassList"
>
<template #append>
<el-button @click="loadClassList" icon="Search"></el-button>
</template>
</el-input>
<el-button
type="danger"
@click="handleBatchDelete"
:disabled="selectedIds.length === 0"
icon="Delete"
style="margin-left: 10px"
>
批量删除
</el-button>
<el-button type="primary" @click="handleAdd" style="margin-left: 10px">新增班级</el-button>
</div>
<!-- 班级表格 -->
<el-table
:data="classList"
border
style="margin-top: 10px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="班级名称" width="200"></el-table-column>
<el-table-column prop="headTeacher" label="班主任" width="200"></el-table-column>
<el-table-column label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)"></el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle">
<el-form :model="form" ref="formRef" label-width="100px">
<el-form-item label="班级名称" prop="name" :rules="[{ required: true, message: '请输入班级名称', trigger: 'blur' }]">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="班主任" prop="headTeacher" :rules="[{ required: true, message: '请输入班主任', trigger: 'blur' }]">
<el-input v-model="form.headTeacher"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getClassList, addClass, editClass, deleteClass, checkClassHasStudent } from '@/api/class'
//
const route = useRoute()
const currentGrade = ref(1)
const currentTitle = ref('一年级班级')
//
const classList = ref([])
//
const searchName = ref('')
//
const selectedIds = ref([])
//
const dialogVisible = ref(false)
const dialogTitle = ref('新增班级')
const formRef = ref(null)
const form = reactive({
id: '',
name: '',
headTeacher: '',
grade: currentGrade.value
})
//
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map(row => row.id)
}
//
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) return
//
const hasStudentClass = []
for (const id of selectedIds.value) {
const res = await checkClassHasStudent(id)
if (res.data.hasStudent) {
const cls = classList.value.find(c => c.id === id)
hasStudentClass.push(cls.name)
}
}
if (hasStudentClass.length > 0) {
ElMessage.error(`以下班级有学生,无法删除:${hasStudentClass.join('、')}`)
return
}
const confirm = await ElMessageBox.confirm(
`确定删除选中的 ${selectedIds.value.length} 个班级?`,
'确认删除',
{ type: 'warning' }
).catch(() => false)
if (confirm) {
try {
await deleteClass(selectedIds.value.join(','))
ElMessage.success('删除成功')
loadClassList()
selectedIds.value = []
} catch (err) {
ElMessage.error('删除失败')
}
}
}
//
const loadClassList = async () => {
const res = await getClassList({
grade: currentGrade.value, // 使
name: searchName.value
})
classList.value = res.data || [] //
}
//
const handleAdd = () => {
dialogTitle.value = '新增班级'
Object.assign(form, {
id: '',
name: '',
headTeacher: '',
grade: currentGrade.value //
})
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑班级'
Object.assign(form, row)
dialogVisible.value = true
}
//
const handleSave = async () => {
await formRef.value.validate()
try {
form.id ? await editClass(form) : await addClass(form)
dialogVisible.value = false
loadClassList()
ElMessage.success('操作成功')
} catch (err) {
ElMessage.error('操作失败')
}
}
//
const handleDelete = async (row) => {
const res = await checkClassHasStudent(row.id)
if (res.data.hasStudent) {
ElMessage.error('班级有学生,无法删除')
return
}
if (await ElMessageBox.confirm(`确定删除【${row.name}】?`, '确认', { type: 'warning' }).catch(() => false)) {
try {
await deleteClass(row.id)
loadClassList()
ElMessage.success('删除成功')
} catch (err) {
ElMessage.error('删除失败')
}
}
}
//
watch(
() => route.meta,
(meta) => {
currentGrade.value = meta.grade || 1 //
currentTitle.value = meta.title || '班级管理' //
form.grade = currentGrade.value //
loadClassList() //
},
{ immediate: true }
)
onMounted(loadClassList)
</script>
<style scoped>
.operate-bar {
display: flex;
gap: 10px;
align-items: center;
}
</style>

@ -0,0 +1,285 @@
<template>
<div class="student-page">
<h2>{{ currentTitle }}</h2>
<!-- 筛选区 + 批量删除按钮 -->
<div class="filter-bar">
<el-select
v-model="selectedClassId"
placeholder="选择班级"
style="width: 200px; margin-right: 10px"
@change="loadStudentList"
>
<el-option label="全部班级" value=""></el-option>
<!-- 修正循环绑定 classList 并确保每个班级有 id name 属性 -->
<el-option
v-for="cls in classList"
:key="cls.id"
:label="cls.name"
:value="cls.id"
></el-option>
</el-select>
<el-input
v-model="searchName"
placeholder="搜索学生姓名"
style="width: 300px; margin-right: 10px"
clearable
@clear="loadStudentList"
>
<template #append>
<el-button @click="loadStudentList" icon="Search"></el-button>
</template>
</el-input>
<!-- 批量删除按钮 -->
<el-button
type="danger"
@click="handleBatchDelete"
:disabled="selectedIds.length === 0"
icon="Delete"
style="margin-right: 10px"
>
批量删除
</el-button>
<el-button type="primary" @click="handleAdd"></el-button>
</div>
<!-- 学生表格带复选框 -->
<el-table
:data="studentList"
border
style="margin-top: 10px"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名" width="120"></el-table-column>
<el-table-column prop="age" label="年龄" width="80"></el-table-column>
<el-table-column prop="gender" label="性别" width="80">
<template #default="scope">{{ scope.row.gender === 1 ? '男' : '女' }}</template>
</el-table-column>
<el-table-column prop="className" label="班级" width="150"></el-table-column>
<el-table-column prop="address" label="家庭住址"></el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)"></el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)"></el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
style="margin-top: 10px; text-align: right"
@current-change="loadStudentList"
@size-change="loadStudentList"
layout="total, sizes, prev, pager, next, jumper"
></el-pagination>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle">
<el-form :model="form" ref="formRef" label-width="100px">
<el-form-item label="姓名" prop="name" :rules="[{ required: true, message: '请输入姓名', trigger: 'blur' }]">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="年龄" prop="age" :rules="[{ required: true, message: '请输入年龄', trigger: 'blur' }]">
<el-input v-model.number="form.age" type="number"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.gender">
<el-radio :label="1"></el-radio>
<el-radio :label="2"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="班级" prop="classId" :rules="[{ required: true, message: '请选择班级', trigger: 'change' }]">
<el-select v-model="form.classId" placeholder="选择班级">
<el-option
v-for="cls in classList"
:key="cls.id"
:label="cls.name"
:value="cls.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="家庭住址" prop="address">
<el-input v-model="form.address" type="textarea" rows="3"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getStudentList, addStudent, editStudent, deleteStudent } from '@/api/student'
import { getClassList } from '@/api/class'
// -
const route = useRoute()
const currentGrade = ref(1)
const currentTitle = ref('一年级学生')
//
const classList = ref([])
//
const studentList = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
//
const searchName = ref('')
const selectedClassId = ref('')
//
const selectedIds = ref([])
//
const dialogVisible = ref(false)
const dialogTitle = ref('新增学生')
const formRef = ref(null)
const form = reactive({
id: '',
name: '',
age: null,
gender: 1,
classId: '',
address: '',
grade: currentGrade.value //
})
//
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map(row => row.id)
}
//
const handleBatchDelete = async () => {
if (selectedIds.value.length === 0) return
const confirm = await ElMessageBox.confirm(
`确定删除选中的 ${selectedIds.value.length} 名学生?`,
'确认删除',
{ type: 'warning' }
).catch(() => false)
if (confirm) {
try {
await deleteStudent(selectedIds.value.join(','))
ElMessage.success('删除成功')
loadStudentList()
selectedIds.value = []
} catch (err) {
ElMessage.error('删除失败')
}
}
}
// -
const loadClassList = async () => {
const res = await getClassList({ grade: currentGrade.value })
// classListidname
classList.value = res?.data || []
}
// -
const loadStudentList = async () => {
//
const page = Math.max(currentPage.value, 1)
const size = Math.min(Math.max(pageSize.value, 1), 100)
const res = await getStudentList({
page,
size,
grade: currentGrade.value, // 使
classId: selectedClassId.value,
name: searchName.value
})
studentList.value = res?.list || [] //
total.value = res?.total || 0
}
//
const handleAdd = () => {
dialogTitle.value = '新增学生'
Object.assign(form, {
id: '',
name: '',
age: null,
classId: '',
address: '',
grade: currentGrade.value //
})
dialogVisible.value = true
}
//
const handleEdit = (row) => {
dialogTitle.value = '编辑学生'
Object.assign(form, row)
dialogVisible.value = true
}
//
const handleSave = async () => {
await formRef.value.validate()
try {
form.id ? await editStudent(form) : await addStudent(form)
dialogVisible.value = false
loadStudentList()
ElMessage.success('操作成功')
} catch (err) {
ElMessage.error('操作失败')
}
}
//
const handleDelete = async (id) => {
if (await ElMessageBox.confirm('确定删除?', '确认', { type: 'warning' }).catch(() => false)) {
try {
await deleteStudent(id)
loadStudentList()
ElMessage.success('删除成功')
} catch (err) {
ElMessage.error('删除失败')
}
}
}
// -
watch(
() => route.meta,
(meta) => {
currentGrade.value = meta.grade || 1 //
currentTitle.value = meta.title || '学生管理' //
form.grade = currentGrade.value //
currentPage.value = 1 //
selectedClassId.value = '' //
loadClassList() //
loadStudentList() //
},
{ immediate: true }
)
onMounted(async () => {
await loadClassList()
await loadStudentList()
})
</script>
<style scoped>
.filter-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
</style>

@ -0,0 +1,37 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// import AutoImport from 'unplugin-auto-import/vite'
// import Components from 'unplugin-vue-components/vite'
// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
server: {
proxy: {
// 匹配所有以 '/api' 开头的请求,转发到后端
'/api': {
target: 'http://localhost:8080', // 后端接口地址
changeOrigin: true, // 允许跨域
}
}
},
plugins: [
vue(),
vueDevTools(),
// AutoImport({
// resolvers: [ElementPlusResolver()],
// }),
// Components({
// resolvers: [ElementPlusResolver()],
// }),
],
//添加的别名
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})
Loading…
Cancel
Save