feat(auth): 实现用户菜单及登出功能
- 在Header组件添加用户下拉菜单,支持显示用户名和操作选项 - 新增点击文档隐藏菜单的事件监听与清理 - 实现登出功能,调用后端登出接口,清理登录状态并跳转主页 - 路由新增管理员页面/admid及其组件admid.vue - 删除unused的首页index.vue页面文件 - 后端新增登出接口/logout,支持用户会话注销 - 修正登录服务实现,修复密码匹配逻辑错误 - 客户端api新增logout接口调用后端登出功能
This commit is contained in:
@@ -33,6 +33,18 @@ public class LoginController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("logout")
|
||||||
|
@ApiOperationLog(description = "登出")
|
||||||
|
public Response<Void> logout() {
|
||||||
|
try {
|
||||||
|
StpUtil.logout();
|
||||||
|
return Response.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("登出失败 {}", e.getMessage());
|
||||||
|
return Response.fail("登出失败 " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("sendVerificationCode")
|
@PostMapping("sendVerificationCode")
|
||||||
@ApiOperationLog(description = "发送验证码")
|
@ApiOperationLog(description = "发送验证码")
|
||||||
public Response<Void> sendVerificationCode(@RequestBody VerificationCodeReqVO verificationCodeReqVO) {
|
public Response<Void> sendVerificationCode(@RequestBody VerificationCodeReqVO verificationCodeReqVO) {
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ public class LoginServiceImpl implements LoginService {
|
|||||||
|
|
||||||
if (reqPassword != null && passwordEncoder.matches(reqPassword, userDO.getPassword())) {
|
if (reqPassword != null && passwordEncoder.matches(reqPassword, userDO.getPassword())) {
|
||||||
StpUtil.login(userDO.getId());
|
StpUtil.login(userDO.getId());
|
||||||
throw new RuntimeException("密码错误");
|
return;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException("登录错误");
|
throw new RuntimeException("登录错误");
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ export function login(data) {
|
|||||||
return axios.post("/login/login", data)
|
return axios.post("/login/login", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return axios.post("/login/logout")
|
||||||
|
}
|
||||||
|
|
||||||
export function getVerificationCode(data) {
|
export function getVerificationCode(data) {
|
||||||
return axios.post("/login/sendVerificationCode", data)
|
return axios.post("/login/sendVerificationCode", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,31 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="flex items-center lg:order-2">
|
<div class="flex items-center lg:order-2">
|
||||||
<template v-if="userName">
|
<template v-if="userName">
|
||||||
<span
|
<div class="relative" ref="menuRef">
|
||||||
class="text-gray-800 dark:text-white font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2">
|
<button
|
||||||
{{ userName }}
|
@click="menuOpen = !menuOpen"
|
||||||
</span>
|
class="text-gray-800 dark:text-white font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 flex items-center">
|
||||||
|
<span class="mr-2">{{ userName }}</span>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="menuOpen"
|
||||||
|
class="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded shadow z-50">
|
||||||
|
<router-link
|
||||||
|
to="/admid"
|
||||||
|
@click="menuOpen = false"
|
||||||
|
class="block px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
||||||
|
后台
|
||||||
|
</router-link>
|
||||||
|
<button
|
||||||
|
@click="handleLogout"
|
||||||
|
class="w-full text-left block px-4 py-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600">
|
||||||
|
登出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a href="#" @click.prevent="showLogin = true"
|
<a href="#" @click.prevent="showLogin = true"
|
||||||
@@ -19,10 +40,6 @@
|
|||||||
Login
|
Login
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
<a href="#"
|
|
||||||
class="text-white bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:bg-primary-600 dark:hover:bg-primary-700 focus:outline-none dark:focus:ring-primary-800">
|
|
||||||
Get started
|
|
||||||
</a>
|
|
||||||
<button data-collapse-toggle="mobile-menu-2" type="button"
|
<button data-collapse-toggle="mobile-menu-2" type="button"
|
||||||
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
class="inline-flex items-center p-2 ml-1 text-sm text-gray-500 rounded-lg lg:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||||
aria-controls="mobile-menu-2" aria-expanded="false">
|
aria-controls="mobile-menu-2" aria-expanded="false">
|
||||||
@@ -74,11 +91,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import LoginDialog from '@/layouts/components/LoginDialog.vue'
|
import LoginDialog from '@/layouts/components/LoginDialog.vue'
|
||||||
import { getUserInfo } from '@/api/user'
|
import { getUserInfo, logout } from '@/api/user'
|
||||||
|
import { removeToken } from '@/composables/auth'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showMessage } from '@/composables/util.js'
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
|
const menuOpen = ref(false)
|
||||||
|
const menuRef = ref(null)
|
||||||
|
const router = useRouter()
|
||||||
async function refreshUser() {
|
async function refreshUser() {
|
||||||
try {
|
try {
|
||||||
const r = await getUserInfo()
|
const r = await getUserInfo()
|
||||||
@@ -88,7 +111,29 @@ async function refreshUser() {
|
|||||||
userName.value = ''
|
userName.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
async function handleLogout() {
|
||||||
|
try {
|
||||||
|
await logout()
|
||||||
|
} finally {
|
||||||
|
removeToken()
|
||||||
|
userName.value = ''
|
||||||
|
menuOpen.value = false
|
||||||
|
showMessage('已退出登录', 'success')
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onDocClick(e) {
|
||||||
|
if (!menuOpen.value) return
|
||||||
|
const el = menuRef.value
|
||||||
|
if (el && !el.contains(e.target)) {
|
||||||
|
menuOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshUser()
|
refreshUser()
|
||||||
|
document.addEventListener('click', onDocClick)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', onDocClick)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
19
enlish-vue/src/pages/admid/admid.vue
Normal file
19
enlish-vue/src/pages/admid/admid.vue
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div class="common-layout">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<Header></Header>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<el-main class="p-4">
|
||||||
|
管理学生
|
||||||
|
</el-main>
|
||||||
|
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Header from '@/layouts/components/Header.vue'
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
<template>
|
|
||||||
<div class="common-layout">
|
|
||||||
<el-container>
|
|
||||||
<el-header>
|
|
||||||
<Header></Header>
|
|
||||||
</el-header>
|
|
||||||
|
|
||||||
<el-container>
|
|
||||||
<el-aside width="200px">
|
|
||||||
Aside
|
|
||||||
</el-aside>
|
|
||||||
|
|
||||||
<el-main>
|
|
||||||
Main
|
|
||||||
</el-main>
|
|
||||||
</el-container>
|
|
||||||
|
|
||||||
</el-container>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import Header from '@/layouts/components/Header.vue'
|
|
||||||
|
|
||||||
</script>
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import Index from '@/pages/index.vue'
|
|
||||||
import Uploadpng from '@/pages/uploadpng.vue'
|
import Uploadpng from '@/pages/uploadpng.vue'
|
||||||
import LearningPlan from '@/pages/LearningPlan.vue'
|
import LearningPlan from '@/pages/LearningPlan.vue'
|
||||||
import Class from '@/pages/class.vue'
|
import Class from '@/pages/class.vue'
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
import Admid from '@/pages/admid/admid.vue'
|
||||||
import Student from '@/pages/student.vue'
|
import Student from '@/pages/student.vue'
|
||||||
|
|
||||||
// 统一在这里声明所有路由
|
// 统一在这里声明所有路由
|
||||||
@@ -34,6 +34,13 @@ const routes = [
|
|||||||
meta: { // meta 信息
|
meta: { // meta 信息
|
||||||
title: '学生详情' // 页面标题
|
title: '学生详情' // 页面标题
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admid', // 路由地址
|
||||||
|
component: Admid, // 对应组件
|
||||||
|
meta: { // meta 信息
|
||||||
|
title: '管理员页面' // 页面标题
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user