插件系统初步

This commit is contained in:
biss committed 2026-10-01 08:24:32 +08:00
1 parent ae4447841a
commit 7e3f138875
17 files changed
+9921 -12

No files matched your search

+18 -1
View File
@@ -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
View File
@@ -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
+57
View File
@@ -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