Compare commits

..

No commits in common. 'f0f4467beae444c18e85dcff22f4efa6d472b060' and '2a9388ddf48bb7351f3727aa5145c1d55a1930b7' have entirely different histories.

@ -28,7 +28,6 @@
"icon": "esno ./scripts/icon.ts"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@iconify/iconify": "^3.1.1",
"@iconify/vue": "^4.3.0",
"@vueuse/core": "^12.3.0",

@ -8,9 +8,6 @@ importers:
.:
dependencies:
'@element-plus/icons-vue':
specifier: ^2.3.2
version: 2.3.2(vue@3.5.13(typescript@5.7.3))
'@iconify/iconify':
specifier: ^3.1.1
version: 3.1.1
@ -904,8 +901,8 @@ packages:
'@dual-bundle/import-meta-resolve@4.1.0':
resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==}
'@element-plus/icons-vue@2.3.2':
resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==}
'@element-plus/icons-vue@2.3.1':
resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==}
peerDependencies:
vue: ^3.2.0
@ -1382,26 +1379,22 @@ packages:
resolution: {integrity: sha512-NAmhw1l/llM0HZRpagR/ChJTNymW4ll6/4EDSJML5c8L5Hl/+k6UyF8EIgE6DeHpfheQujkSRngauViHqq6jJQ==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.0.0-rc.1':
resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.0.1':
resolution: {integrity: sha512-5RFH8x+Mn3mbjcHXnb6KCXGiczBdiQkWkv99iiA0JpKrNuTAQeW59Pjq/uObMB0eR0shnKYGTkIJxum+DbL3sw==}
engines: {node: '>= 16'}
'@intlify/message-compiler@12.0.0-alpha.3':
resolution: {integrity: sha512-mDDTN3gfYOHhBnpnlby19UHyvMaOnzdlpsIrxUfs44R/vCATfn8pMOkE8PXD2t410xkocEj3FpDcC9XC/0v4Dg==}
'@intlify/shared@11.0.0-rc.1':
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==}
engines: {node: '>= 16'}
'@intlify/shared@11.0.1':
resolution: {integrity: sha512-lH164+aDDptHZ3dBDbIhRa1dOPQUp+83iugpc+1upTOWCnwyC1PVis6rSWNMMJ8VQxvtHQB9JMib48K55y0PvQ==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.12':
resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==}
engines: {node: '>= 16'}
'@intlify/shared@12.0.0-alpha.3':
resolution: {integrity: sha512-ryaNYBvxQjyJUmVuBBg+HHUsmGnfxcEUPR0NCeG4/K9N2qtyFE35C80S15IN6iYFE2MGWLN7HfOSyg0MXZIc9w==}
engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@6.0.3':
resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==}
engines: {node: '>= 18'}
@ -2809,7 +2802,7 @@ packages:
engines: {node: '>=18'}
errno@0.1.8:
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
resolution: {integrity: sha1-i7Ppx9Rjvkl2/4iPdrSAnrwugR8=}
hasBin: true
error-ex@1.3.2:
@ -3442,7 +3435,7 @@ packages:
engines: {node: '>= 4'}
image-size@0.5.5:
resolution: {integrity: sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==}
resolution: {integrity: sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=}
engines: {node: '>=0.10.0'}
hasBin: true
@ -4025,7 +4018,7 @@ packages:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
make-dir@2.1.0:
resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==}
resolution: {integrity: sha1-XwMQ4YuL6JjMBwCSlaMK5B6R5vU=}
engines: {node: '>=6'}
make-iterator@1.0.1:
@ -4095,7 +4088,7 @@ packages:
engines: {node: '>= 0.6'}
mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
resolution: {integrity: sha1-Ms2eXGRVO9WNGaVor0Uqz/BJgbE=}
engines: {node: '>=4'}
hasBin: true
@ -5006,7 +4999,7 @@ packages:
engines: {node: '>=0.10.0'}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
resolution: {integrity: sha1-dHIq8y6WFOnCh6jQu95IteLxomM=}
engines: {node: '>=0.10.0'}
source-map@0.7.4:
@ -6543,7 +6536,7 @@ snapshots:
'@dual-bundle/import-meta-resolve@4.1.0': {}
'@element-plus/icons-vue@2.3.2(vue@3.5.13(typescript@5.7.3))':
'@element-plus/icons-vue@2.3.1(vue@3.5.13(typescript@5.7.3))':
dependencies:
vue: 3.5.13(typescript@5.7.3)
@ -6902,8 +6895,8 @@ snapshots:
'@intlify/bundle-utils@10.0.0(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))':
dependencies:
'@intlify/message-compiler': 12.0.0-alpha.3
'@intlify/shared': 12.0.0-alpha.3
'@intlify/message-compiler': 11.0.0-rc.1
'@intlify/shared': 11.0.0-rc.1
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 2.0.2
@ -6919,28 +6912,26 @@ snapshots:
'@intlify/message-compiler': 11.0.1
'@intlify/shared': 11.0.1
'@intlify/message-compiler@11.0.1':
'@intlify/message-compiler@11.0.0-rc.1':
dependencies:
'@intlify/shared': 11.0.1
'@intlify/shared': 11.0.0-rc.1
source-map-js: 1.2.1
'@intlify/message-compiler@12.0.0-alpha.3':
'@intlify/message-compiler@11.0.1':
dependencies:
'@intlify/shared': 12.0.0-alpha.3
'@intlify/shared': 11.0.1
source-map-js: 1.2.1
'@intlify/shared@11.0.1': {}
'@intlify/shared@11.1.12': {}
'@intlify/shared@11.0.0-rc.1': {}
'@intlify/shared@12.0.0-alpha.3': {}
'@intlify/shared@11.0.1': {}
'@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@9.17.0(jiti@2.4.2))(rollup@4.30.1)(typescript@5.7.3)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2))
'@intlify/bundle-utils': 10.0.0(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))
'@intlify/shared': 11.1.12
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.13)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@intlify/shared': 11.0.1
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.0.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@rollup/pluginutils': 5.1.4(rollup@4.30.1)
'@typescript-eslint/scope-manager': 8.19.1
'@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3)
@ -6962,11 +6953,11 @@ snapshots:
- supports-color
- typescript
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.12)(@vue/compiler-dom@3.5.13)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.0.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.0.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@babel/parser': 7.26.3
optionalDependencies:
'@intlify/shared': 11.1.12
'@intlify/shared': 11.0.1
'@vue/compiler-dom': 3.5.13
vue: 3.5.13(typescript@5.7.3)
vue-i18n: 11.0.1(vue@3.5.13(typescript@5.7.3))
@ -8562,7 +8553,7 @@ snapshots:
element-plus@2.9.2(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@ctrl/tinycolor': 3.6.1
'@element-plus/icons-vue': 2.3.2(vue@3.5.13(typescript@5.7.3))
'@element-plus/icons-vue': 2.3.1(vue@3.5.13(typescript@5.7.3))
'@floating-ui/dom': 1.6.13
'@popperjs/core': '@sxzz/popperjs-es@2.11.7'
'@types/lodash': 4.17.14

@ -1,26 +0,0 @@
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 } })
}

@ -1,21 +0,0 @@
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}`)
}

@ -5,7 +5,7 @@ import { useI18n } from '@/hooks/web/useI18n'
import { ThemeSwitch } from '@/components/ThemeSwitch'
import { useCssVar } from '@vueuse/core'
import { useAppStore } from '@/store/modules/app'
import { trim, setCssVar } from '@/utils'
import { trim, setCssVar, getCssVar } from '@/utils'
import ColorRadioPicker from './components/ColorRadioPicker.vue'
import InterfaceDisplay from './components/InterfaceDisplay.vue'
import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
@ -147,6 +147,12 @@ const clear = () => {
storageClear()
window.location.reload()
}
const themeChange = () => {
const color = getCssVar('--el-bg-color')
setMenuTheme(color)
setHeaderTheme(color)
}
</script>
<template>
@ -166,7 +172,7 @@ const clear = () => {
<div class="text-center">
<!-- 主题 -->
<ElDivider>{{ t('setting.theme') }}</ElDivider>
<ThemeSwitch />
<ThemeSwitch @change="themeChange" />
<!-- 布局 -->
<ElDivider>{{ t('setting.layout') }}</ElDivider>

@ -51,7 +51,7 @@ const confirm = () => {
const newColumns = cloneDeep(unref(settingColumns))?.map((item) => {
const fixed = unref(settingColumns)?.find((col) => col.field === item.field)?.fixed
item.hidden = !unref(checkColumns)?.includes(item.field)
item.fixed = fixed ? fixed : false
item.fixed = fixed ? fixed : undefined
return item
})
emit('confirm', [...unref(hiddenColumns), ...(newColumns || [])])
@ -65,7 +65,7 @@ const restore = () => {
const initColumns = (columns: TableColumn[], isReStore = false) => {
const newColumns = columns?.filter((item) => {
if (!isReStore) {
item.fixed = item.fixed !== void 0 ? item.fixed : false
item.fixed = item.fixed !== void 0 ? item.fixed : undefined
}
return (item.type && !DEFAULT_FILTER_COLUMN.includes(item.type)) || !item.type
})
@ -133,18 +133,18 @@ watch(
:key="item.field"
class="flex items-center justify-between mt-12px"
>
<ElCheckbox :value="item.field">
<ElCheckbox :label="item.field">
{{ item.label }}
</ElCheckbox>
<div class="flex items-center">
<ElRadioGroup size="small" v-model="item.fixed">
<ElRadioButton value="left">
<ElRadioButton label="left">
<Icon icon="vi-ep:arrow-left" />
</ElRadioButton>
<ElRadioButton :value="false">
<ElRadioButton :label="undefined">
<Icon icon="vi-ep:close" />
</ElRadioButton>
<ElRadioButton value="right">
<ElRadioButton label="right">
<Icon icon="vi-ep:arrow-right" />
</ElRadioButton>
</ElRadioGroup>

@ -1,10 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue'
import { ref } from 'vue'
import { useAppStore } from '@/store/modules/app'
import { ElSwitch } from 'element-plus'
import { useIcon } from '@/hooks/web/useIcon'
import { useDesign } from '@/hooks/web/useDesign'
import { getCssVar } from '@/utils'
const { getPrefixCls } = useDesign()
@ -19,21 +18,15 @@ const CrescentMoon = useIcon({ icon: 'vi-emojione-monotone:crescent-moon', color
const appStore = useAppStore()
//
const isDark = computed({
get() {
return appStore.getIsDark
},
set(val: boolean) {
appStore.setIsDark(val)
const color = getCssVar('--el-bg-color')
appStore.setMenuTheme(color)
appStore.setHeaderTheme(color)
emit('change', val)
}
})
const isDark = ref(appStore.getIsDark)
// switch
const blackColor = 'var(--el-color-black)'
const themeChange = (val: boolean) => {
appStore.setIsDark(val)
emit('change', val)
}
</script>
<template>
@ -46,6 +39,7 @@ const blackColor = 'var(--el-color-black)'
:active-color="blackColor"
:active-icon="Sun"
:inactive-icon="CrescentMoon"
@change="themeChange"
/>
</template>

@ -15,6 +15,9 @@ import { setupStore } from '@/store'
// 全局组件
import { setupGlobCom } from '@/components'
// 引入element-plus
import { setupElementPlus } from '@/plugins/elementPlus'
// 引入全局样式
import '@/styles/index.less'
@ -28,19 +31,10 @@ import { setupRouter } from './router'
import { setupPermission } from './directives'
import { createApp } from 'vue'
import App from './App.vue'
import './permission'
// ===========================================
// 1. 完整导入 ElementPlus 和它的所有图标
import ElementPlus from 'element-plus'
// 2. 完整导入 ElementPlus 的样式文件
import 'element-plus/dist/index.css'
// 3. 完整导入 ElementPlus 的所有图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
// ============================================
import './permission'
// 创建实例
const setupAll = async () => {
@ -52,17 +46,7 @@ const setupAll = async () => {
setupGlobCom(app)
// ============================================
// 4. 全局注册 ElementPlus
app.use(ElementPlus)
// 5. 全局注册所有 ElementPlus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component as any)
}
// ===========================================
setupElementPlus(app)
setupRouter(app)

@ -18,7 +18,6 @@ router.beforeEach(async (to, from, next) => {
const permissionStore = usePermissionStoreWithOut()
const appStore = useAppStoreWithOut()
const userStore = useUserStoreWithOut()
if (userStore.getUserInfo) {
if (to.path === '/login') {
next({ path: '/' })
@ -28,51 +27,38 @@ router.beforeEach(async (to, from, next) => {
return
}
// ======================================================
// 1. 强制禁用动态路由,使用静态路由模式
// 这会忽略后端返回的路由和角色权限,直接加载 asyncRouterMap 中的所有路由
appStore.setDynamicRouter(false)
// 开发者可根据实际情况进行修改
const roleRouters = userStore.getRoleRouters || []
// 2. 加载静态路由
// 'static' 模式会让 permissionStore 加载 asyncRouterMap 中的所有路由
await permissionStore.generateRoutes('static')
// ======================================================
// 是否使用动态路由
if (appStore.getDynamicRouter) {
appStore.serverDynamicRouter
? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[])
: await permissionStore.generateRoutes('frontEnd', roleRouters as string[])
} else {
await permissionStore.generateRoutes('static')
}
// 将生成的路由添加到 router 实例中
permissionStore.getAddRouters.forEach((route) => {
router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
})
// 处理重定向
const redirectPath = from.query.redirect || to.path
const redirect = decodeURIComponent(redirectPath as string)
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
// 标记路由已添加,防止重复添加
permissionStore.setIsAddRouters(true)
// 跳转到目标页面
next(nextData)
}
} else {
// 用户未登录
if (NO_REDIRECT_WHITE_LIST.indexOf(to.path) !== -1) {
// 在白名单内的路径可以直接访问
next()
} else {
// 否则重定向到登录页,并携带目标路径作为 redirect 参数
next(`/login?redirect=${to.path}`)
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
}
}
})
router.afterEach((to) => {
// 设置页面标题
useTitle(to?.meta?.title as string)
// 结束进度条
done()
// 结束页面加载动画
done() // 结束Progress
loadDone()
})

@ -1,14 +1,24 @@
import type { App } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 需要全局引入一些组件如ElScrollbar不然一些下拉项样式有问题
import { ElLoading, ElScrollbar } from 'element-plus'
const plugins = [ElLoading]
const components = [ElScrollbar]
export const setupElementPlus = (app: App<Element>) => {
// 1. 完整注册 ElementPlus
app.use(ElementPlus)
plugins.forEach((plugin) => {
app.use(plugin)
})
// 2. 全局注册所有 Element Plus 图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
// 为了开发环境启动更快,一次性引入所有样式
if (import.meta.env.VITE_USE_ALL_ELEMENT_PLUS_STYLE === 'true') {
import('element-plus/dist/index.css')
return
}
components.forEach((component) => {
app.component(component.name!, component)
})
}

@ -4,8 +4,6 @@ import type { App } from 'vue'
import { Layout, getParentLayout } from '@/utils/routerHelper'
import { useI18n } from '@/hooks/web/useI18n'
import { NO_RESET_WHITE_LIST } from '@/constants'
import ClassGrade from '@/views/student/ClassGrade.vue' // 静态导入 ClassGrade 组件
import StudentManagement from '@/views/student/StudentManagement.vue' // 静态导入 StudentManagement 组件
const { t } = useI18n()
@ -729,103 +727,7 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
}
}
]
},
// ============================================
{
path: '/student-system',
component: Layout,
redirect: '/student-system/class/grade1',
name: 'StudentSystem',
meta: {
title: '学生管理系统',
icon: 'vi-ep:user',
alwaysShow: true
},
children: [
// 班级管理
{
path: 'class',
component: getParentLayout(),
redirect: '/student-system/class/grade1',
name: 'ClassManagementGroup',
meta: {
title: '班级管理',
alwaysShow: true
},
children: [
{
path: 'grade1',
name: 'ClassGrade1',
component: ClassGrade,
meta: {
title: '一年级班级',
grade: 1
}
},
{
path: 'grade2',
name: 'ClassGrade2',
component: ClassGrade,
meta: {
title: '二年级班级',
grade: 2
}
},
{
path: 'grade3',
name: 'ClassGrade3',
component: () => ClassGrade,
meta: {
title: '三年级班级',
grade: 3
}
}
]
},
// 学生管理
{
path: 'student',
component: getParentLayout(),
redirect: '/student-system/student/grade1',
name: 'StudentManagementGroup',
meta: {
title: '学生管理',
alwaysShow: true
},
children: [
{
path: 'grade1',
name: 'StudentGrade1',
component: StudentManagement,
meta: {
title: '一年级学生',
grade: 1
}
},
{
path: 'grade2',
name: 'StudentGrade2',
component: StudentManagement,
meta: {
title: '二年级学生',
grade: 2
}
},
{
path: 'grade3',
name: 'StudentGrade3',
component: StudentManagement,
meta: {
title: '三年级学生',
grade: 3
}
}
]
}
]
}
//=======================================
]
const router = createRouter({
@ -849,5 +751,3 @@ export const setupRouter = (app: App<Element>) => {
}
export default router
console.log('路由列表:', router.getRoutes())

@ -328,8 +328,6 @@ export const useAppStore = defineStore('app', {
valueLight: 'light'
})
isDark.value = this.getIsDark
const newTitle = import.meta.env.VITE_APP_TITLE
newTitle !== this.getTitle && this.setTitle(newTitle)
}
},
persist: true

@ -1,22 +0,0 @@
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

@ -3,7 +3,7 @@ import { LoginForm, RegisterForm } from './components'
import { ThemeSwitch } from '@/components/ThemeSwitch'
import { LocaleDropdown } from '@/components/LocaleDropdown'
import { useI18n } from '@/hooks/web/useI18n'
import { underlineToHump } from '@/utils'
import { getCssVar, underlineToHump } from '@/utils'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { ref } from 'vue'
@ -26,6 +26,12 @@ const toRegister = () => {
const toLogin = () => {
isLogin.value = true
}
const themeChange = () => {
const color = getCssVar('--el-bg-color')
appStore.setMenuTheme(color)
appStore.setHeaderTheme(color)
}
</script>
<template>
@ -66,7 +72,7 @@ const toLogin = () => {
</div>
<div class="flex justify-end items-center space-x-10px">
<ThemeSwitch />
<ThemeSwitch @change="themeChange" />
<LocaleDropdown class="lt-xl:text-white dark:text-white" />
</div>
</div>

@ -76,7 +76,6 @@ const schema = reactive<FormSchema[]>([
// enter
onKeydown: (_e: any) => {
if (_e.key === 'Enter') {
_e.stopPropagation() //
signIn()
}
}

@ -1,232 +0,0 @@
<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" />
</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 prop="id" label="ID" width="80" />
<el-table-column prop="name" label="班级名称" width="200" />
<el-table-column prop="headTeacher" label="班主任" width="200" />
<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-form-item>
<el-form-item
label="班主任"
prop="headTeacher"
:rules="[{ required: true, message: '请输入班主任', trigger: 'blur' }]"
>
<el-input v-model="form.headTeacher" />
</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>

@ -1,287 +0,0 @@
<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="" />
<!-- 修正循环绑定 classList 并确保每个班级有 id name 属性 -->
<el-option v-for="cls in classList" :key="cls.id" :label="cls.name" :value="cls.id" />
</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" />
</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 prop="id" label="ID" width="80" />
<el-table-column prop="name" label="姓名" width="120" />
<el-table-column prop="age" label="年龄" width="80" />
<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 prop="address" label="家庭住址" />
<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-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-form-item>
<el-form-item
label="年龄"
prop="age"
:rules="[{ required: true, message: '请输入年龄', trigger: 'blur' }]"
>
<el-input v-model.number="form.age" type="number" />
</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-select>
</el-form-item>
<el-form-item label="家庭住址" prop="address">
<el-input v-model="form.address" type="textarea" rows="3" />
</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>

@ -145,9 +145,9 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
proxy: {
// 选项写法
'/api': {
// 已将目标地址从 8000 修改为 8080
target: 'http://127.0.0.1:8080',
changeOrigin: true
target: 'http://127.0.0.1:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
},
hmr: {

Loading…
Cancel
Save