插件系统初步
This commit is contained in:
1 parent
ae4447841a
commit
7e3f138875
17 files changed
+9921
-12
No files matched your search
@@ -5,6 +5,7 @@ import { Bell, Operation } from '@element-plus/icons-vue'
|
||||
import http from '../api/http'
|
||||
import { branding } from '../config/branding'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { usePluginStore } from '../stores/plugins'
|
||||
|
||||
const unreadNotifCount = ref(0)
|
||||
|
||||
@@ -30,6 +31,7 @@ interface NavigationGroup {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const plugins = usePluginStore()
|
||||
const mobileMenu = ref(false)
|
||||
|
||||
const roles = computed(() => auth.user?.roles ?? [])
|
||||
@@ -62,6 +64,13 @@ function whenVisible(visible: boolean, item: NavigationItem) {
|
||||
return visible ? [item] : []
|
||||
}
|
||||
|
||||
function isNavigationItemEnabled(item: NavigationItem) {
|
||||
const owner = plugins.items.find((plugin) =>
|
||||
plugin.navigationPaths.includes(item.path),
|
||||
)
|
||||
return !owner || owner.isEnabled
|
||||
}
|
||||
|
||||
const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
{
|
||||
key: 'overview',
|
||||
@@ -120,6 +129,7 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
{ path: '/students', label: '学生档案' },
|
||||
),
|
||||
...whenVisible(auth.isSuperAdmin, { path: '/users', label: '用户与权限' }),
|
||||
...whenVisible(auth.isSuperAdmin, { path: '/plugins', label: '插件中心' }),
|
||||
...whenVisible(auth.isSuperAdmin, { path: '/operations', label: '运维与审计' }),
|
||||
],
|
||||
},
|
||||
@@ -301,7 +311,12 @@ const navigationGroups = computed<NavigationGroup[]>(() => [
|
||||
),
|
||||
],
|
||||
},
|
||||
].filter((group) => group.items.length > 0))
|
||||
]
|
||||
.map((group) => ({
|
||||
...group,
|
||||
items: group.items.filter(isNavigationItemEnabled),
|
||||
}))
|
||||
.filter((group) => group.items.length > 0))
|
||||
|
||||
const activeGroupKey = computed(
|
||||
() => navigationGroups.value.find((group) =>
|
||||
@@ -328,12 +343,14 @@ const pageTitle = computed(() => {
|
||||
})
|
||||
|
||||
function signOut() {
|
||||
plugins.reset()
|
||||
auth.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
auth.refresh().catch(() => undefined)
|
||||
plugins.load().catch(() => undefined)
|
||||
pollUnread()
|
||||
setInterval(pollUnread, 60000)
|
||||
})
|
||||
|
||||
+40
-11
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { usePluginStore } from '../stores/plugins'
|
||||
import AdminLayout from '../layouts/AdminLayout.vue'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -159,7 +160,7 @@ const router = createRouter({
|
||||
path: 'academic-planning',
|
||||
name: 'academic-planning',
|
||||
component: () => import('../views/AcademicPlanningView.vue'),
|
||||
meta: { roles: ['Student'] },
|
||||
meta: { roles: ['Student'], pluginId: 'mingxu.graduation' },
|
||||
},
|
||||
{
|
||||
path: 'teaching-tasks',
|
||||
@@ -232,25 +233,26 @@ const router = createRouter({
|
||||
path: 'classroom-reservations',
|
||||
name: 'classroom-reservations',
|
||||
component: () => import('../views/ClassroomReservationsView.vue'),
|
||||
meta: { pluginId: 'mingxu.campus-services' },
|
||||
},
|
||||
{
|
||||
path: 'campus-registrations',
|
||||
name: 'campus-registrations',
|
||||
component: () => import('../views/CampusRegistrationsView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'], pluginId: 'mingxu.campus-services' },
|
||||
},
|
||||
{
|
||||
path: 'campus-wallets',
|
||||
name: 'campus-wallets',
|
||||
component: () => import('../views/CampusWalletManagementView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin'], pluginId: 'mingxu.campus-services' },
|
||||
},
|
||||
{ path: 'tuition', name: 'tuition', component: () => import('../views/TuitionView.vue'), meta: { roles: ['SuperAdmin','AcademicAdmin','Student'] } },
|
||||
{ path: 'tuition', name: 'tuition', component: () => import('../views/TuitionView.vue'), meta: { roles: ['SuperAdmin','AcademicAdmin','Student'], pluginId: 'mingxu.campus-services' } },
|
||||
{
|
||||
path: 'classroom-repairs',
|
||||
name: 'classroom-repairs',
|
||||
component: () => import('../views/ClassroomRepairsView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'MaintenanceAdmin', 'MaintenanceStaff'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'MaintenanceAdmin', 'MaintenanceStaff'], pluginId: 'mingxu.campus-services' },
|
||||
},
|
||||
{
|
||||
path: 'experiments',
|
||||
@@ -258,6 +260,7 @@ const router = createRouter({
|
||||
component: () => import('../views/ExperimentsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student'],
|
||||
pluginId: 'mingxu.experiments',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -266,6 +269,7 @@ const router = createRouter({
|
||||
component: () => import('../views/ExperimentGradesView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student'],
|
||||
pluginId: 'mingxu.experiments',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -316,6 +320,7 @@ const router = createRouter({
|
||||
component: () => import('../views/GradeAnalyticsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader', 'Teacher'],
|
||||
pluginId: 'mingxu.statistics',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -324,31 +329,32 @@ const router = createRouter({
|
||||
component: () => import('../views/EventAnalyticsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'],
|
||||
pluginId: 'mingxu.statistics',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'other-exams',
|
||||
name: 'other-exams',
|
||||
component: () => import('../views/OtherExamsView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Student'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Student'], pluginId: 'mingxu.examinations' },
|
||||
},
|
||||
{
|
||||
path: 'physical-fitness',
|
||||
name: 'physical-fitness',
|
||||
component: () => import('../views/PhysicalFitnessView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'PhysicalEducationAdmin', 'CollegeAdmin', 'Counselor', 'Student'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'PhysicalEducationAdmin', 'CollegeAdmin', 'Counselor', 'Student'], pluginId: 'mingxu.physical-fitness' },
|
||||
},
|
||||
{
|
||||
path: 'exams',
|
||||
name: 'exams',
|
||||
component: () => import('../views/ExamsView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student'], pluginId: 'mingxu.examinations' },
|
||||
},
|
||||
{
|
||||
path: 'makeup-exams',
|
||||
name: 'makeup-exams',
|
||||
component: () => import('../views/MakeupExamsView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Teacher', 'Student'], pluginId: 'mingxu.examinations' },
|
||||
},
|
||||
{
|
||||
path: 'course-adjustments',
|
||||
@@ -380,7 +386,7 @@ const router = createRouter({
|
||||
path: 'warnings',
|
||||
name: 'warnings',
|
||||
component: () => import('../views/WarningsView.vue'),
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Counselor', 'Student'] },
|
||||
meta: { roles: ['SuperAdmin', 'AcademicAdmin', 'Counselor', 'Student'], pluginId: 'mingxu.student-services' },
|
||||
},
|
||||
{
|
||||
path: 'student-status-changes',
|
||||
@@ -388,6 +394,7 @@ const router = createRouter({
|
||||
component: () => import('../views/StudentStatusChangesView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'],
|
||||
pluginId: 'mingxu.student-services',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -396,6 +403,7 @@ const router = createRouter({
|
||||
component: () => import('../views/OfficialDocumentsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
|
||||
pluginId: 'mingxu.student-services',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -404,6 +412,7 @@ const router = createRouter({
|
||||
component: () => import('../views/GraduationAuditsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
|
||||
pluginId: 'mingxu.graduation',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -412,6 +421,7 @@ const router = createRouter({
|
||||
component: () => import('../views/DegreeAwardsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Student'],
|
||||
pluginId: 'mingxu.graduation',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -420,6 +430,7 @@ const router = createRouter({
|
||||
component: () => import('../views/GraduationClearanceView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Counselor', 'Student'],
|
||||
pluginId: 'mingxu.graduation',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -434,12 +445,19 @@ const router = createRouter({
|
||||
component: () => import('../views/OperationsConsoleView.vue'),
|
||||
meta: { roles: ['SuperAdmin'] },
|
||||
},
|
||||
{
|
||||
path: 'plugins',
|
||||
name: 'plugins',
|
||||
component: () => import('../views/PluginsView.vue'),
|
||||
meta: { roles: ['SuperAdmin'] },
|
||||
},
|
||||
{
|
||||
path: 'evaluations',
|
||||
name: 'evaluations',
|
||||
component: () => import('../views/EvaluationView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Teacher', 'Student', 'Leader'],
|
||||
pluginId: 'mingxu.evaluations',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -448,6 +466,7 @@ const router = createRouter({
|
||||
component: () => import('../views/StatisticsView.vue'),
|
||||
meta: {
|
||||
roles: ['SuperAdmin', 'AcademicAdmin', 'CollegeAdmin', 'Leader'],
|
||||
pluginId: 'mingxu.statistics',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -456,7 +475,7 @@ const router = createRouter({
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
router.beforeEach(async (to) => {
|
||||
const auth = useAuthStore()
|
||||
if (!to.meta.public && !auth.isLoggedIn) {
|
||||
return { name: 'login', query: { redirect: to.fullPath } }
|
||||
@@ -466,6 +485,16 @@ router.beforeEach((to) => {
|
||||
if (roles && !roles.some((role) => auth.user?.roles.includes(role))) {
|
||||
return { name: 'dashboard' }
|
||||
}
|
||||
const pluginId = to.meta.pluginId as string | undefined
|
||||
if (pluginId) {
|
||||
const plugins = usePluginStore()
|
||||
try {
|
||||
await plugins.load()
|
||||
} catch {
|
||||
// Preserve the existing route when the runtime manifest is temporarily unavailable.
|
||||
}
|
||||
if (!plugins.isEnabled(pluginId)) return { name: 'dashboard' }
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,57 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import http from '../api/http'
|
||||
|
||||
export interface PluginRuntimeItem {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
category: string
|
||||
isEnabled: boolean
|
||||
navigationPaths: string[]
|
||||
capabilities: string[]
|
||||
stateChangedAt?: string
|
||||
}
|
||||
|
||||
export const usePluginStore = defineStore('plugins', () => {
|
||||
const items = ref<PluginRuntimeItem[]>([])
|
||||
const loaded = ref(false)
|
||||
let loadingPromise: Promise<void> | undefined
|
||||
|
||||
const byId = computed(() => new Map(items.value.map((item) => [item.id, item])))
|
||||
|
||||
function isEnabled(pluginId?: string) {
|
||||
if (!pluginId || !loaded.value) return true
|
||||
return byId.value.get(pluginId)?.isEnabled ?? false
|
||||
}
|
||||
|
||||
async function load(force = false) {
|
||||
if (loaded.value && !force) return
|
||||
if (loadingPromise && !force) return loadingPromise
|
||||
loadingPromise = http.get<PluginRuntimeItem[]>('/plugins/runtime')
|
||||
.then(({ data }) => {
|
||||
items.value = data
|
||||
loaded.value = true
|
||||
})
|
||||
.finally(() => {
|
||||
loadingPromise = undefined
|
||||
})
|
||||
return loadingPromise
|
||||
}
|
||||
|
||||
function update(item: PluginRuntimeItem) {
|
||||
const index = items.value.findIndex((candidate) => candidate.id === item.id)
|
||||
if (index >= 0) items.value[index] = item
|
||||
else items.value.push(item)
|
||||
loaded.value = true
|
||||
}
|
||||
|
||||
function reset() {
|
||||
items.value = []
|
||||
loaded.value = false
|
||||
loadingPromise = undefined
|
||||
}
|
||||
|
||||
return { items, loaded, isEnabled, load, update, reset }
|
||||
})
|
||||
Loaded 3 of 17 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user