添加管理端角色和基础框架

This commit is contained in:
柏码の讲师 2024-12-23 00:58:56 +08:00
parent ac51f828d7
commit 4de9ed28df
9 changed files with 287 additions and 169 deletions

View File

@ -56,7 +56,7 @@ public class SecurityConfiguration {
.requestMatchers("/api/auth/**", "/error").permitAll() .requestMatchers("/api/auth/**", "/error").permitAll()
.requestMatchers("/images/**").permitAll() .requestMatchers("/images/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT) .anyRequest().hasAnyRole(Const.ROLE_DEFAULT, Const.ROLE_ADMIN)
) )
.formLogin(conf -> conf .formLogin(conf -> conf
.loginProcessingUrl("/api/auth/login") .loginProcessingUrl("/api/auth/login")

View File

@ -22,6 +22,7 @@ public final class Const {
public final static String MQ_MAIL = "mail"; public final static String MQ_MAIL = "mail";
//用户角色 //用户角色
public final static String ROLE_DEFAULT = "user"; public final static String ROLE_DEFAULT = "user";
public final static String ROLE_ADMIN = "admin";
//论坛相关 //论坛相关
public final static String FORUM_WEATHER_CACHE = "weather:cache:"; public final static String FORUM_WEATHER_CACHE = "weather:cache:";
public final static String FORUM_IMAGE_COUNTER = "forum:image:"; public final static String FORUM_IMAGE_COUNTER = "forum:image:";

View File

@ -7,7 +7,7 @@ spring:
mail: mail:
host: smtp.163.com host: smtp.163.com
username: javastudy111@163.com username: javastudy111@163.com
password: QGLYGBVECIASGPWQ password: SFxfE39P9Wf2icKi
rabbitmq: rabbitmq:
addresses: localhost addresses: localhost
username: admin username: admin
@ -44,4 +44,4 @@ spring:
username: 'minio' username: 'minio'
password: 'password' password: 'password'
weather: weather:
key: 7abbc24d3b4443b597a3b3c676e0f221 key: 115bcaf74fb24f73844365290d85862b

View File

@ -11,13 +11,16 @@ useDark({
useDark({ useDark({
onChanged(dark) { useToggle(dark) } onChanged(dark) { useToggle(dark) }
}) })
</script> </script>
<template> <template>
<header> <header>
<div class="wrapper"> <div class="wrapper">
<router-view/> <router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"/>
</keep-alive>
</router-view>
</div> </div>
</header> </header>
</template> </template>

View File

@ -0,0 +1,88 @@
<script setup>
import router from "@/router";
import {Back, Message, Operation, Right} from "@element-plus/icons-vue";
import {useStore} from "@/store";
import {logout} from "@/net";
defineProps({
admin: Boolean
})
const store = useStore()
function userLogout() {
logout(() => router.push("/"))
}
</script>
<template>
<div class="user-info">
<el-button type="primary" size="small"
@click="router.push('/index')"
v-if="admin">
回到用户端
<el-icon style="margin-left: 5px">
<Right/>
</el-icon>
</el-button>
<el-button type="danger" size="small"
@click="router.push('/admin')"
v-else>
前往管理端
<el-icon style="margin-left: 5px">
<Right/>
</el-icon>
</el-button>
<template/>
<div class="profile">
<div>{{ store.user.username }}</div>
<div>{{ store.user.email }}</div>
</div>
<el-dropdown>
<el-avatar :src="store.avatarUrl"/>
<template #dropdown>
<el-dropdown-item>
<el-icon>
<Operation/>
</el-icon>
个人设置
</el-dropdown-item>
<el-dropdown-item>
<el-icon>
<Message/>
</el-icon>
消息列表
</el-dropdown-item>
<el-dropdown-item @click="userLogout" divided>
<el-icon>
<Back/>
</el-icon>
退出登录
</el-dropdown-item>
</template>
</el-dropdown>
</div>
</template>
<style scoped>
.user-info {
display: flex;
gap: 20px;
align-items: center;
.profile {
text-align: right;
:first-child {
font-size: 18px;
font-weight: bold;
line-height: 20px;
}
:last-child {
font-size: 10px;
color: grey;
}
}
}
</style>

View File

@ -53,6 +53,13 @@ const router = createRouter({
component: () => import('@/views/settings/PrivacySetting.vue') component: () => import('@/views/settings/PrivacySetting.vue')
} }
] ]
}, {
path: '/admin',
name: 'admin',
component: () => import('@/views/AdminView.vue'),
children: [
]
} }
] ]
}) })

View File

@ -17,6 +17,9 @@ export const useStore = defineStore('general', {
} }
} }
}, getters: { }, getters: {
isAdmin() {
return this.user.role === 'admin'
},
avatarUrl() { avatarUrl() {
if(this.user.avatar) if(this.user.avatar)
return `${axios.defaults.baseURL}/images${this.user.avatar}` return `${axios.defaults.baseURL}/images${this.user.avatar}`

View File

@ -0,0 +1,117 @@
<script setup>
import {
Bell,
ChatDotSquare, Collection,
DataLine,
Document,
Files,
Location,
Monitor, Notification, Position, School,
Umbrella,
User
} from "@element-plus/icons-vue";
import UserInfo from "@/components/UserInfo.vue";
import {useStore} from "@/store";
import {ref} from "vue";
import {get} from "@/net";
const adminMenu = [
{
title: '校园论坛管理', icon: Location, sub: [
{title: '用户管理', icon: User},
{title: '帖子广场管理', icon: ChatDotSquare},
{title: '失物招领管理', icon: Bell},
{title: '校园活动管理', icon: Notification},
{title: '表白墙管理', icon: Umbrella},
{title: '合作机构管理', icon: School}
]
}, {
title: '探索与发现管理', icon: Position, sub: [
{title: '成绩查询管理', icon: Document},
{title: '班级课程表管理', icon: Files},
{title: '教务通知管理', icon: Monitor},
{title: '在线图书馆管理', icon: Collection},
{title: '预约教室管理', icon: DataLine}
]
}
]
const store = useStore()
const loading = ref(true)
get('/api/user/info', (data) => {
store.user = data
loading.value = false
})
</script>
<template>
<div class="admin-content" v-loading="loading" element-loading-text="正在进入,请稍后...">
<el-container style="height: 100%">
<el-aside width="230px" class="admin-content-aside">
<div style="text-align: center;padding: 15px 0 10px;height: 32px">
<el-image class="logo" src="https://element-plus.org/images/element-plus-logo.svg"/>
</div>
<el-scrollbar style="height: calc(100% - 60px)">
<el-menu
router
:default-active="$route.path"
:default-openeds="['1', '2', '3']"
style="height: calc(100% - 60px);border-right: none">
<el-sub-menu :index="(index + 1).toString()"
v-for="(menu, index) in adminMenu">
<template #title>
<el-icon>
<component :is="menu.icon"/>
</el-icon>
<span><b>{{ menu.title }}</b></span>
</template>
<el-menu-item :index="subMenu.title" v-for="subMenu in menu.sub">
<template #title>
<el-icon>
<component :is="subMenu.icon"/>
</el-icon>
{{ subMenu.title }}
<el-tag style="margin-left: 10px" size="small"
:type="subMenu.tag.type"
v-if="subMenu.tag">{{ subMenu.tag.name }}
</el-tag>
</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<el-container>
<el-header class="admin-content-header">
<div style="flex: 1"></div>
<user-info admin/>
</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
<style lang="less" scoped>
.admin-content {
height: 100vh;
width: 100vw;
}
.admin-content-aside {
border-right: solid 1px var(--el-border-color);
.logo {
height: 32px;
}
}
.admin-content-header {
border-bottom: solid 1px var(--el-border-color);
height: 55px;
display: flex;
align-items: center;
box-sizing: border-box;
}
</style>

View File

@ -1,20 +1,43 @@
<script setup> <script setup>
import {get, logout} from '@/net' import {get} from '@/net'
import router from "@/router";
import {useStore} from "@/store"; import {useStore} from "@/store";
import {reactive, ref} from "vue"; import {reactive, ref} from "vue";
import { import {
Back,
Bell, Bell,
ChatDotSquare, Check, Collection, DataLine, ChatDotSquare, Check, Collection, DataLine,
Document, Files, Document, Files,
Location, Lock, Message, Monitor, Location, Lock, Monitor,
Notification, Operation, Notification, Operation,
Position, Position,
School, Search, School, Search,
Umbrella, User Umbrella, User
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import LightCard from "@/components/LightCard.vue"; import LightCard from "@/components/LightCard.vue";
import UserInfo from "@/components/UserInfo.vue";
const userMenu = [
{ title: '校园论坛', icon: Location, sub: [
{ title: '帖子广场', index: '/index', icon: ChatDotSquare },
{ title: '失物招领', icon: Bell },
{ title: '校园活动', icon: Notification },
{ title: '表白墙', icon: Umbrella },
{ title: '海文考研', icon: School, tag: { name: '合作机构', type: '' } }
]
}, {
title: '探索与发现', icon: Position, sub: [
{ title: '成绩查询', icon: Document },
{ title: '班级课程表', icon: Files },
{ title: '教务通知', icon: Monitor },
{ title: '在线图书馆', icon: Collection },
{ title: '预约教室', icon: DataLine }
]
}, {
title: '个人设置', icon: Operation, sub: [
{ title: '个人信息设置', icon: User },
{ title: '账号安全设置', icon: Lock }
]
}
]
const store = useStore() const store = useStore()
const loading = ref(true) const loading = ref(true)
@ -25,18 +48,10 @@ const searchInput = reactive({
}) })
const notification = ref([]) const notification = ref([])
get('/api/user/info', (data) => {
store.user = data
loading.value = false
})
const loadNotification = const loadNotification =
() => get('/api/notification/list', data => notification.value = data) () => get('/api/notification/list', data => notification.value = data)
loadNotification() loadNotification()
function userLogout() {
logout(() => router.push("/"))
}
function confirmNotification(id, url) { function confirmNotification(id, url) {
get(`/api/notification/delete?id=${id}`, () => { get(`/api/notification/delete?id=${id}`, () => {
loadNotification() loadNotification()
@ -47,6 +62,11 @@ function confirmNotification(id, url) {
function deleteAllNotification() { function deleteAllNotification() {
get(`/api/notification/delete-all`, loadNotification) get(`/api/notification/delete-all`, loadNotification)
} }
get('/api/user/info', (data) => {
store.user = data
loading.value = false
})
</script> </script>
<template> <template>
@ -72,10 +92,10 @@ function deleteAllNotification() {
</template> </template>
</el-input> </el-input>
</div> </div>
<div class="user-info"> <user-info>
<el-popover placement="bottom" :width="350" trigger="click"> <el-popover placement="bottom" :width="350" trigger="click">
<template #reference> <template #reference>
<el-badge style="margin-right: 15px" is-dot :hidden="!notification.length"> <el-badge is-dot :hidden="!notification.length">
<div class="notification"> <div class="notification">
<el-icon><Bell/></el-icon> <el-icon><Bell/></el-icon>
<div style="font-size: 10px">消息</div> <div style="font-size: 10px">消息</div>
@ -101,34 +121,7 @@ function deleteAllNotification() {
style="width: 100%" plain>清除全部未读消息</el-button> style="width: 100%" plain>清除全部未读消息</el-button>
</div> </div>
</el-popover> </el-popover>
<div class="profile"> </user-info>
<div>{{ store.user.username }}</div>
<div>{{ store.user.email }}</div>
</div>
<el-dropdown>
<el-avatar :src="store.avatarUrl"/>
<template #dropdown>
<el-dropdown-item>
<el-icon>
<Operation/>
</el-icon>
个人设置
</el-dropdown-item>
<el-dropdown-item>
<el-icon>
<Message/>
</el-icon>
消息列表
</el-dropdown-item>
<el-dropdown-item @click="userLogout" divided>
<el-icon>
<Back/>
</el-icon>
退出登录
</el-dropdown-item>
</template>
</el-dropdown>
</div>
</el-header> </el-header>
<el-container> <el-container>
<el-aside width="230px"> <el-aside width="230px">
@ -138,127 +131,26 @@ function deleteAllNotification() {
:default-active="$route.path" :default-active="$route.path"
:default-openeds="['1', '2', '3']" :default-openeds="['1', '2', '3']"
style="min-height: calc(100vh - 55px)"> style="min-height: calc(100vh - 55px)">
<el-sub-menu index="1"> <el-sub-menu :index="(index + 1).toString()"
<template #title> v-for="(menu, index) in userMenu">
<el-icon> <template #title>
<Location/> <el-icon>
</el-icon> <component :is="menu.icon"/>
<span><b>校园论坛</b></span> </el-icon>
</template> <span><b>{{ menu.title }}</b></span>
<el-menu-item index="/index"> </template>
<template #title> <el-menu-item :index="subMenu.index" v-for="subMenu in menu.sub">
<el-icon> <template #title>
<ChatDotSquare/> <el-icon>
</el-icon> <component :is="subMenu.icon"/>
帖子广场 </el-icon>
</template> {{ subMenu.title }}
</el-menu-item> <el-tag style="margin-left: 10px" size="small"
<el-menu-item> :type="subMenu.tag.type"
<template #title> v-if="subMenu.tag">{{ subMenu.tag.name }}</el-tag>
<el-icon> </template>
<Bell/> </el-menu-item>
</el-icon> </el-sub-menu>
失物招领
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Notification/>
</el-icon>
校园活动
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Umbrella/>
</el-icon>
表白墙
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<School/>
</el-icon>
海文考研
<el-tag style="margin-left: 10px" size="small">合作机构</el-tag>
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="2">
<template #title>
<el-icon>
<Position/>
</el-icon>
<span><b>探索与发现</b></span>
</template>
<el-menu-item>
<template #title>
<el-icon>
<Document/>
</el-icon>
成绩查询
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Files/>
</el-icon>
班级课程表
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Monitor/>
</el-icon>
教务通知
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<Collection/>
</el-icon>
在线图书馆
</template>
</el-menu-item>
<el-menu-item>
<template #title>
<el-icon>
<DataLine/>
</el-icon>
预约教室
</template>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="3">
<template #title>
<el-icon>
<Operation/>
</el-icon>
<span><b>个人设置</b></span>
</template>
<el-menu-item index="/index/user-setting">
<template #title>
<el-icon>
<User/>
</el-icon>
个人信息设置
</template>
</el-menu-item>
<el-menu-item index="/index/privacy-setting">
<template #title>
<el-icon>
<Lock/>
</el-icon>
账号安全设置
</template>
</el-menu-item>
</el-sub-menu>
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
</el-aside> </el-aside>
@ -320,9 +212,17 @@ function deleteAllNotification() {
.logo { .logo {
height: 32px; height: 32px;
width: 340px;
text-align: left;
:deep(.el-image__inner) {
width: 120px;
}
} }
.user-info { .user-info {
gap: 20px;
width: 340px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
@ -333,7 +233,6 @@ function deleteAllNotification() {
.profile { .profile {
text-align: right; text-align: right;
margin-right: 20px;
:first-child { :first-child {
font-size: 18px; font-size: 18px;