mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: 实现功能模块系统,支持模块的动态注册与启用控制
- 新增 ModuleRegistry 核心,支持模块注册、可用性检查和启用状态管理 - 新增模块管理 API(管理端和公共端),提供模块状态查询和启用控制 - 将 LDAP 功能迁移到模块系统,支持通过环境变量和数据库配置控制 - 前端新增模块管理页面和 store,支持模块启用/禁用操作 - 路由守卫集成模块状态检查,未激活模块的页面自动重定向 - 面包屑导航支持模块配置页面的层级显示 - Switch 组件新增 disabled 属性支持
This commit is contained in:
269
frontend/src/views/admin/ModuleManagement.vue
Normal file
269
frontend/src/views/admin/ModuleManagement.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="模块管理"
|
||||
description="管理系统功能模块的启用状态"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="outline"
|
||||
:disabled="loading"
|
||||
@click="fetchModules"
|
||||
>
|
||||
<RefreshCw class="w-4 h-4 mr-2" :class="{ 'animate-spin': loading }" />
|
||||
刷新
|
||||
</Button>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="mt-6 mb-6">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索模块名称或描述..."
|
||||
class="pl-11 h-11"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- 模块卡片网格 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
<div
|
||||
v-for="module in filteredModules"
|
||||
:key="module.name"
|
||||
class="group relative border rounded-2xl p-6 transition-all duration-200 hover:shadow-lg"
|
||||
:class="{
|
||||
'bg-muted/40 border-muted': !module.available,
|
||||
'border-primary/40 bg-gradient-to-br from-primary/5 to-primary/10 shadow-sm': module.active,
|
||||
'border-border bg-card hover:border-primary/20': !module.active && module.available
|
||||
}"
|
||||
>
|
||||
<!-- 状态指示器 -->
|
||||
<div class="absolute top-5 right-5">
|
||||
<div
|
||||
class="w-2.5 h-2.5 rounded-full ring-2 ring-offset-2 ring-offset-background"
|
||||
:class="{
|
||||
'bg-green-500 ring-green-500/30': module.active,
|
||||
'bg-amber-500 ring-amber-500/30': module.available && module.enabled && !module.active,
|
||||
'bg-gray-300 ring-gray-300/30': module.available && !module.enabled,
|
||||
'bg-red-400 ring-red-400/30': !module.available
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 模块图标和名称 -->
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div
|
||||
class="w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-colors"
|
||||
:class="module.active
|
||||
? 'bg-primary/15 text-primary'
|
||||
: 'bg-muted text-muted-foreground group-hover:bg-muted/80'"
|
||||
>
|
||||
<component :is="getCategoryIcon(module.category)" class="w-6 h-6" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 pt-0.5">
|
||||
<h4 class="font-semibold text-base truncate">{{ module.display_name }}</h4>
|
||||
<div class="mt-1.5">
|
||||
<Badge
|
||||
:variant="getStatusBadgeVariant(module)"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ getStatusText(module) }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<p class="text-sm text-muted-foreground leading-relaxed line-clamp-2 min-h-[2.5rem]">
|
||||
{{ module.description }}
|
||||
</p>
|
||||
|
||||
<!-- 模块信息 -->
|
||||
<div class="mt-4 pt-4 border-t border-border/50 space-y-2">
|
||||
<div class="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span class="font-mono bg-muted/50 px-1.5 py-0.5 rounded">{{ module.name }}</span>
|
||||
<span class="text-border">|</span>
|
||||
<span :class="{
|
||||
'text-green-600': module.health === 'healthy',
|
||||
'text-amber-600': module.health === 'degraded',
|
||||
'text-red-600': module.health === 'unhealthy',
|
||||
}">
|
||||
{{ getHealthText(module.health) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不可用提示 -->
|
||||
<div
|
||||
v-if="!module.available"
|
||||
class="mt-4 text-xs text-orange-700 dark:text-orange-400 bg-orange-100 dark:bg-orange-950/50 rounded-lg px-3 py-2"
|
||||
>
|
||||
模块不可用,请检查环境变量和依赖库
|
||||
</div>
|
||||
|
||||
<!-- 操作区域 -->
|
||||
<div class="mt-5 pt-4 border-t border-border/50 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<Switch
|
||||
:model-value="module.enabled"
|
||||
:disabled="!module.available || toggling[module.name]"
|
||||
@update:model-value="(val: boolean) => toggleModule(module.name, val)"
|
||||
/>
|
||||
<span class="text-sm" :class="module.enabled ? 'text-foreground' : 'text-muted-foreground'">
|
||||
{{ module.enabled ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
v-if="module.admin_route && module.active"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="gap-1.5"
|
||||
@click="router.push(module.admin_route)"
|
||||
>
|
||||
<Settings class="w-3.5 h-3.5" />
|
||||
配置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索无结果 -->
|
||||
<div
|
||||
v-if="filteredModules.length === 0 && searchQuery && !loading"
|
||||
class="text-center py-16"
|
||||
>
|
||||
<Search class="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||
<p class="text-muted-foreground">没有找到匹配的模块</p>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div
|
||||
v-if="allModules.length === 0 && !loading"
|
||||
class="text-center py-16"
|
||||
>
|
||||
<Puzzle class="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||
<p class="text-muted-foreground">暂无可管理的模块</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RefreshCw, Puzzle, Users, Shield, Gauge, Link, Search, Settings } from 'lucide-vue-next'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Badge from '@/components/ui/badge.vue'
|
||||
import Switch from '@/components/ui/switch.vue'
|
||||
import Input from '@/components/ui/input.vue'
|
||||
import { PageHeader, PageContainer } from '@/components/layout'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useModuleStore } from '@/stores/modules'
|
||||
import type { ModuleStatus } from '@/api/modules'
|
||||
import { log } from '@/utils/logger'
|
||||
|
||||
const router = useRouter()
|
||||
const { success, error } = useToast()
|
||||
const moduleStore = useModuleStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const toggling = ref<Record<string, boolean>>({})
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 获取分类图标
|
||||
function getCategoryIcon(category: string) {
|
||||
const icons: Record<string, any> = {
|
||||
auth: Users,
|
||||
monitoring: Gauge,
|
||||
security: Shield,
|
||||
integration: Link,
|
||||
}
|
||||
return icons[category] || Puzzle
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(module: ModuleStatus): string {
|
||||
if (!module.available) return '不可用'
|
||||
if (module.active) return '已激活'
|
||||
if (module.enabled) return '已启用'
|
||||
return '已禁用'
|
||||
}
|
||||
|
||||
// 获取状态徽章样式
|
||||
function getStatusBadgeVariant(module: ModuleStatus): 'default' | 'secondary' | 'outline' | 'destructive' {
|
||||
if (!module.available) return 'destructive'
|
||||
if (module.active) return 'default'
|
||||
if (module.enabled) return 'secondary'
|
||||
return 'outline'
|
||||
}
|
||||
|
||||
// 获取健康状态文本
|
||||
function getHealthText(health: string): string {
|
||||
const texts: Record<string, string> = {
|
||||
healthy: '健康',
|
||||
degraded: '降级',
|
||||
unhealthy: '异常',
|
||||
unknown: '未知',
|
||||
}
|
||||
return texts[health] || health
|
||||
}
|
||||
|
||||
// 所有模块列表(按 admin_menu_order 排序)
|
||||
const allModules = computed(() => {
|
||||
return Object.values(moduleStore.modules)
|
||||
.sort((a, b) => a.admin_menu_order - b.admin_menu_order)
|
||||
})
|
||||
|
||||
// 过滤后的模块列表
|
||||
const filteredModules = computed(() => {
|
||||
if (!searchQuery.value.trim()) {
|
||||
return allModules.value
|
||||
}
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
return allModules.value.filter(
|
||||
(m) =>
|
||||
m.name.toLowerCase().includes(query) ||
|
||||
m.display_name.toLowerCase().includes(query) ||
|
||||
m.description.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
// 获取模块列表
|
||||
async function fetchModules() {
|
||||
loading.value = true
|
||||
try {
|
||||
await moduleStore.fetchModules()
|
||||
} catch (err) {
|
||||
error('获取模块列表失败')
|
||||
log.error('获取模块列表失败:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换模块启用状态
|
||||
async function toggleModule(moduleName: string, enabled: boolean) {
|
||||
toggling.value[moduleName] = true
|
||||
try {
|
||||
const result = await moduleStore.setEnabled(moduleName, enabled)
|
||||
if (result) {
|
||||
success(enabled ? '模块已启用' : '模块已禁用')
|
||||
} else {
|
||||
error('操作失败')
|
||||
}
|
||||
} catch (err) {
|
||||
error('操作失败')
|
||||
log.error('切换模块状态失败:', err)
|
||||
} finally {
|
||||
toggling.value[moduleName] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchModules()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user