20 changed files with 964 additions and 321 deletions
@ -1,253 +1,192 @@ |
|||
import { createRouter, createWebHashHistory } from 'vue-router' |
|||
import { useUserStore } from '@/stores/user' |
|||
|
|||
const router = createRouter({ |
|||
history: createWebHashHistory(), |
|||
routes: [ |
|||
{ |
|||
path: '/', |
|||
redirect: '/login', |
|||
}, |
|||
{ |
|||
path: '/:pathMatch(.*)*', |
|||
redirect: '/login', |
|||
redirect: '/user/chat/flow', |
|||
}, |
|||
{ |
|||
path: '/login', |
|||
name: 'Login', |
|||
component: () => import('@/views/login/Login.vue'), |
|||
}, |
|||
// ========== 企业AI 门户 ==========
|
|||
{ |
|||
path: '/user', |
|||
component: () => import('@/components/layout/MainLayout.vue'), |
|||
redirect: '/user/dashboard', |
|||
component: () => import('@/components/layout/AdminLayout.vue'), |
|||
children: [ |
|||
{ |
|||
path: 'dashboard', |
|||
name: 'Dashboard', |
|||
component: () => import('@/views/dashboard/Dashboard.vue'), |
|||
meta: { title: '工作台' }, |
|||
}, |
|||
{ |
|||
path: 'chat/flow', |
|||
name: 'FlowChat', |
|||
name: 'UserAIHelper', |
|||
component: () => import('@/views/chat/FlowChat.vue'), |
|||
meta: { title: '流式对话' }, |
|||
meta: { title: 'AI 助手' }, |
|||
}, |
|||
{ |
|||
path: 'agent/list', |
|||
name: 'AgentList', |
|||
component: () => import('@/views/agent/AgentList.vue'), |
|||
meta: { title: '智能体' }, |
|||
}, |
|||
{ |
|||
path: 'agent/chat/:type', |
|||
name: 'AgentChat', |
|||
component: () => import('@/views/agent/AgentChat.vue'), |
|||
meta: { title: '智能体对话' }, |
|||
path: 'task/published', |
|||
name: 'UserTaskPublished', |
|||
component: () => import('@/views/task/TaskList.vue'), |
|||
meta: { title: '发布的任务' }, |
|||
}, |
|||
{ |
|||
path: 'document/manager', |
|||
name: 'DocumentManager', |
|||
component: () => import('@/views/document/DocumentManager.vue'), |
|||
meta: { title: '文档管理' }, |
|||
path: 'task/received', |
|||
name: 'UserTaskReceived', |
|||
component: () => import('@/views/task/TaskList.vue'), |
|||
meta: { title: '收到的任务' }, |
|||
}, |
|||
{ |
|||
path: 'task/list', |
|||
name: 'TaskList', |
|||
component: () => import('@/views/task/TaskList.vue'), |
|||
meta: { title: '任务列表', perms: ['task:read'] }, |
|||
path: 'task/create', |
|||
name: 'UserTaskCreate', |
|||
component: () => import('@/views/task/TaskCreate.vue'), |
|||
meta: { title: '创建任务' }, |
|||
}, |
|||
{ |
|||
path: 'task/:id', |
|||
name: 'TaskDetail', |
|||
name: 'UserTaskDetail', |
|||
component: () => import('@/views/task/TaskDetail.vue'), |
|||
meta: { title: '任务详情', perms: ['task:read'] }, |
|||
meta: { title: '任务详情' }, |
|||
}, |
|||
{ |
|||
path: 'notification/center', |
|||
name: 'NotificationCenter', |
|||
path: 'notification', |
|||
name: 'UserNotification', |
|||
component: () => import('@/views/notification/NotificationCenter.vue'), |
|||
meta: { title: '通知中心' }, |
|||
}, |
|||
{ |
|||
path: 'monitor/employees', |
|||
name: 'UserMonitorEmployees', |
|||
component: () => import('@/views/monitor/EmployeeList.vue'), |
|||
meta: { title: '员工效率' }, |
|||
}, |
|||
{ |
|||
path: 'profile', |
|||
name: 'Profile', |
|||
name: 'UserProfile', |
|||
component: () => import('@/views/profile/Profile.vue'), |
|||
meta: { title: '个人中心' }, |
|||
}, |
|||
{ |
|||
path: ':pathMatch(.*)*', |
|||
redirect: '/user/chat/flow', |
|||
}, |
|||
], |
|||
}, |
|||
// ========== 管理后台 ==========
|
|||
{ |
|||
path: '/admin', |
|||
component: () => import('@/components/layout/AdminLayout.vue'), |
|||
redirect: '/admin', |
|||
children: [ |
|||
{ |
|||
path: '', |
|||
name: 'AdminDashboard', |
|||
component: () => import('@/views/dashboard/Dashboard.vue'), |
|||
meta: { title: '控制台', perms: ['admin:access'] }, |
|||
redirect: '/admin/model/providers', |
|||
}, |
|||
// ---- 系统管理 ----
|
|||
{ |
|||
path: 'org/departments', |
|||
name: 'AdminDepartments', |
|||
component: () => import('@/views/org/DepartmentTree.vue'), |
|||
meta: { title: '部门管理', perms: ['user:read'] }, |
|||
path: 'model/providers', |
|||
name: 'AdminModelProviders', |
|||
component: () => import('@/views/model/ModelProviderManager.vue'), |
|||
meta: { title: '模型管理' }, |
|||
}, |
|||
{ |
|||
path: 'org/users', |
|||
name: 'AdminUserList', |
|||
component: () => import('@/views/org/UserList.vue'), |
|||
meta: { title: '人员管理', perms: ['user:read'] }, |
|||
path: 'rbac/roles', |
|||
name: 'AdminRbacRoles', |
|||
component: () => import('@/views/rbac/RoleList.vue'), |
|||
meta: { title: '平台角色' }, |
|||
}, |
|||
{ |
|||
path: 'role/list', |
|||
name: 'AdminRoleList', |
|||
component: () => import('@/views/role/RoleList.vue'), |
|||
meta: { title: '角色管理', perms: ['role:read'] }, |
|||
path: 'positions/manage', |
|||
name: 'AdminPositionsManage', |
|||
component: () => import('@/views/rbac/PositionManage.vue'), |
|||
meta: { title: '用户岗位配置' }, |
|||
}, |
|||
{ |
|||
path: 'role/:id/permissions', |
|||
name: 'AdminRolePermissions', |
|||
component: () => import('@/views/role/PermissionConfig.vue'), |
|||
meta: { title: '权限配置', perms: ['role:read'] }, |
|||
path: 'org/departments', |
|||
name: 'AdminOrgDepartments', |
|||
component: () => import('@/views/org/DepartmentTree.vue'), |
|||
meta: { title: '组织架构' }, |
|||
}, |
|||
{ |
|||
path: 'flow/list', |
|||
name: 'AdminFlowList', |
|||
component: () => import('@/views/flow/FlowList.vue'), |
|||
meta: { title: '流列表', perms: ['flow:read'] }, |
|||
path: 'org/users', |
|||
name: 'AdminOrgUsers', |
|||
component: () => import('@/views/org/UserList.vue'), |
|||
meta: { title: '人员管理' }, |
|||
}, |
|||
// ---- AI 应用 ----
|
|||
{ |
|||
path: 'flow/editor', |
|||
name: 'AdminFlowEditor', |
|||
component: () => import('@/views/flow/FlowEditor.vue'), |
|||
meta: { title: '流编辑器', perms: ['flow:create'] }, |
|||
meta: { title: '流程编辑器' }, |
|||
}, |
|||
{ |
|||
path: 'flow/editor/:id', |
|||
name: 'AdminFlowEditorEdit', |
|||
component: () => import('@/views/flow/FlowEditor.vue'), |
|||
meta: { title: '编辑流', perms: ['flow:update'] }, |
|||
meta: { title: '编辑流程' }, |
|||
}, |
|||
{ |
|||
path: 'flow/list', |
|||
name: 'AdminFlowList', |
|||
component: () => import('@/views/flow/FlowList.vue'), |
|||
meta: { title: '流程列表' }, |
|||
}, |
|||
{ |
|||
path: 'flow/market', |
|||
name: 'AdminFlowMarket', |
|||
component: () => import('@/views/flow/FlowMarket.vue'), |
|||
meta: { title: '流市场', perms: ['flow:read'] }, |
|||
meta: { title: '流程市场' }, |
|||
}, |
|||
{ |
|||
path: 'flow/market/:id', |
|||
name: 'AdminFlowDetail', |
|||
component: () => import('@/views/flow/FlowDetail.vue'), |
|||
meta: { title: '流详情', perms: ['flow:read'] }, |
|||
path: 'rag/knowledge', |
|||
name: 'AdminRagKnowledge', |
|||
component: () => import('@/views/rag/KnowledgeBase.vue'), |
|||
meta: { title: '知识库管理' }, |
|||
}, |
|||
// ---- 工具集成 ----
|
|||
{ |
|||
path: 'tools/custom', |
|||
name: 'AdminCustomToolManager', |
|||
path: 'custom-tools', |
|||
name: 'AdminCustomTools', |
|||
component: () => import('@/views/tools/CustomToolManager.vue'), |
|||
meta: { title: '自定义API工具', perms: ['flow:create'] }, |
|||
meta: { title: '自定义工具' }, |
|||
}, |
|||
{ |
|||
path: 'mcp/manager', |
|||
name: 'AdminMcpManager', |
|||
path: 'mcp/servers', |
|||
name: 'AdminMcpServers', |
|||
component: () => import('@/views/mcp/McpManager.vue'), |
|||
meta: { title: 'MCP服务管理', perms: ['flow:create'] }, |
|||
}, |
|||
{ |
|||
path: 'rag/knowledge', |
|||
name: 'AdminKnowledgeBase', |
|||
component: () => import('@/views/rag/KnowledgeBase.vue'), |
|||
meta: { title: '知识库管理', perms: ['flow:create'] }, |
|||
meta: { title: 'MCP 服务注册' }, |
|||
}, |
|||
{ |
|||
path: 'wecom/config', |
|||
name: 'AdminWecomConfig', |
|||
component: () => import('@/views/wecom/BotConfig.vue'), |
|||
meta: { title: '企微机器人配置', perms: ['admin:access'] }, |
|||
}, |
|||
{ |
|||
path: 'model/providers', |
|||
name: 'AdminModelProviders', |
|||
component: () => import('@/views/model/ModelProviderManager.vue'), |
|||
meta: { title: '模型供应商管理', perms: ['admin:access'] }, |
|||
}, |
|||
{ |
|||
path: 'monitor/employees', |
|||
name: 'AdminMonitorEmployees', |
|||
component: () => import('@/views/monitor/EmployeeList.vue'), |
|||
meta: { title: '员工监控', perms: ['monitor:read'] }, |
|||
}, |
|||
{ |
|||
path: 'monitor/:id/dashboard', |
|||
name: 'AdminMonitorDashboard', |
|||
component: () => import('@/views/monitor/WorkDashboard.vue'), |
|||
meta: { title: '工作看板', perms: ['monitor:read'] }, |
|||
}, |
|||
{ |
|||
path: 'monitor/:id/analysis', |
|||
name: 'AdminMonitorAnalysis', |
|||
component: () => import('@/views/monitor/AIAnalysis.vue'), |
|||
meta: { title: 'AI分析', perms: ['monitor:read'] }, |
|||
meta: { title: '企微机器人配置' }, |
|||
}, |
|||
// ---- 审计日志 ----
|
|||
{ |
|||
path: 'task/create', |
|||
name: 'AdminTaskCreate', |
|||
component: () => import('@/views/task/TaskCreate.vue'), |
|||
meta: { title: '创建任务', perms: ['task:create'] }, |
|||
path: 'audit/logs', |
|||
name: 'AdminAuditLogs', |
|||
component: () => import('@/views/audit/AuditLog.vue'), |
|||
meta: { title: '操作日志' }, |
|||
}, |
|||
{ |
|||
path: 'audit', |
|||
name: 'AdminAudit', |
|||
component: () => import('@/views/audit/AuditLog.vue'), |
|||
meta: { title: '审计日志', perms: ['audit:read'] }, |
|||
path: 'notification/manage', |
|||
name: 'AdminNotificationManage', |
|||
component: () => import('@/views/notification/NotificationCenter.vue'), |
|||
meta: { title: '通知管理' }, |
|||
}, |
|||
{ |
|||
path: 'system/monitor', |
|||
name: 'AdminSystemMonitor', |
|||
path: 'system/dashboard', |
|||
name: 'AdminSystemDashboard', |
|||
component: () => import('@/views/system/SystemMonitor.vue'), |
|||
meta: { title: '系统监控', perms: ['audit:read'] }, |
|||
meta: { title: '系统监控' }, |
|||
}, |
|||
{ |
|||
path: 'settings', |
|||
name: 'AdminSettings', |
|||
component: () => import('@/views/settings/Settings.vue'), |
|||
meta: { title: '系统设置', perms: ['audit:read'] }, |
|||
path: ':pathMatch(.*)*', |
|||
redirect: '/admin/model/providers', |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}) |
|||
|
|||
router.beforeEach(async (to, _from) => { |
|||
const userStore = useUserStore() |
|||
|
|||
if (userStore.token) { |
|||
if (to.name === 'Login' || to.path === '/') { |
|||
return { name: 'Dashboard' } |
|||
} |
|||
if (!userStore.user) { |
|||
try { |
|||
await userStore.fetchUser() |
|||
} catch { |
|||
userStore.logout() |
|||
return { name: 'Login', query: { redirect: to.fullPath } } |
|||
} |
|||
if (!userStore.isLoggedIn) { |
|||
return { name: 'Login', query: { redirect: to.fullPath } } |
|||
} |
|||
} |
|||
if (to.meta.perms && Array.isArray(to.meta.perms) && to.meta.perms.length > 0) { |
|||
if (!userStore.hasPermission(to.meta.perms[0])) { |
|||
return { name: 'Dashboard' } |
|||
} |
|||
} |
|||
return true |
|||
} |
|||
|
|||
if (to.name === 'Login') { return true } |
|||
return { name: 'Login', query: { redirect: to.fullPath } } |
|||
}) |
|||
|
|||
export default router |
|||
@ -1,38 +1,199 @@ |
|||
<template> |
|||
<div class="monitor-page"> |
|||
<div> |
|||
<el-card> |
|||
<template #header> |
|||
<span>员工工作监控</span> |
|||
</template> |
|||
<el-table :data="employees" v-loading="loading"> |
|||
<el-table-column prop="display_name" label="姓名" /> |
|||
<el-table-column prop="position" label="岗位" /> |
|||
<el-table-column prop="email" label="邮箱" /> |
|||
<el-table-column label="操作" width="250"> |
|||
<el-table-column prop="username" label="用户名" width="140" /> |
|||
<el-table-column prop="display_name" label="显示名称" width="140" /> |
|||
<el-table-column prop="position" label="岗位" width="140" /> |
|||
<el-table-column prop="email" label="邮箱" min-width="200" /> |
|||
<el-table-column label="操作" width="200" fixed="right"> |
|||
<template #default="{ row }"> |
|||
<el-button size="small" type="primary" @click="$router.push(`/admin/monitor/${row.id}/dashboard`)">工作看板</el-button> |
|||
<el-button size="small" type="success" @click="$router.push(`/admin/monitor/${row.id}/analysis`)">AI分析</el-button> |
|||
<el-button size="small" type="primary" @click="openDashboard(row)">工作看板</el-button> |
|||
<el-button size="small" type="success" @click="openAnalysis(row)">AI分析</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</el-card> |
|||
|
|||
<!-- 工作看板右侧抽屉 --> |
|||
<el-drawer v-model="dashboardVisible" :title="`工作看板 - ${currentEmployee?.display_name}`" direction="rtl" size="520px"> |
|||
<div v-if="dashboardLoading" style="text-align: center; padding: 40px;"> |
|||
<el-icon class="is-loading" :size="32"><Loading /></el-icon> |
|||
</div> |
|||
<div v-else-if="dashboardData" class="drawer-content"> |
|||
<el-row :gutter="12" style="margin-bottom: 16px;"> |
|||
<el-col :span="8"> |
|||
<el-card shadow="hover" class="stat-card"> |
|||
<div class="stat-num">{{ dashboardData.stats?.total_messages || 0 }}</div> |
|||
<div class="stat-label">总消息数</div> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :span="8"> |
|||
<el-card shadow="hover" class="stat-card"> |
|||
<div class="stat-num">{{ dashboardData.stats?.total_sessions || 0 }}</div> |
|||
<div class="stat-label">总会话数</div> |
|||
</el-card> |
|||
</el-col> |
|||
<el-col :span="8"> |
|||
<el-card shadow="hover" class="stat-card"> |
|||
<div class="stat-num">{{ dashboardData.stats?.active_days || 0 }}</div> |
|||
<div class="stat-label">活跃天数</div> |
|||
</el-card> |
|||
</el-col> |
|||
</el-row> |
|||
|
|||
<el-card shadow="hover" style="margin-bottom: 16px;"> |
|||
<template #header><span style="font-weight: 600;">消息分类统计</span></template> |
|||
<div v-if="breakdownList.length > 0"> |
|||
<div v-for="(item, i) in breakdownList" :key="i" |
|||
style="display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f0f0f0;"> |
|||
<el-tag size="small" :type="item.role === 'user' ? '' : item.role === 'assistant' ? 'success' : 'warning'"> |
|||
{{ item.role }} |
|||
</el-tag> |
|||
<span style="font-weight: 500;">{{ item.count }}</span> |
|||
</div> |
|||
</div> |
|||
<div v-else style="color: #c0c4cc;">暂无数据</div> |
|||
</el-card> |
|||
|
|||
<el-card shadow="hover"> |
|||
<template #header><span style="font-weight: 600;">最近对话</span></template> |
|||
<div v-if="recentInteractions.length > 0"> |
|||
<div v-for="(r, i) in recentInteractions" :key="i" |
|||
style="margin-bottom: 12px; padding: 10px; background: #f9fafb; border-radius: 8px;"> |
|||
<div style="display: flex; gap: 8px; margin-bottom: 4px;"> |
|||
<el-tag size="small" :type="r.role === 'user' ? '' : 'success'">{{ r.role }}</el-tag> |
|||
<span style="font-size: 12px; color: #909399;">{{ r.created_at?.slice(0, 16) }}</span> |
|||
</div> |
|||
<div style="font-size: 13px; color: #606266; word-break: break-all;">{{ r.content?.slice(0, 300) }}</div> |
|||
</div> |
|||
</div> |
|||
<div v-else style="color: #c0c4cc;">暂无数据</div> |
|||
</el-card> |
|||
</div> |
|||
</el-drawer> |
|||
|
|||
<!-- AI分析右侧抽屉 --> |
|||
<el-drawer v-model="analysisVisible" :title="`AI分析 - ${currentEmployee?.display_name}`" direction="rtl" size="520px"> |
|||
<div v-if="analysisLoading" style="text-align: center; padding: 40px;"> |
|||
<el-icon class="is-loading" :size="32"><Loading /></el-icon> |
|||
<p style="color: #909399; margin-top: 12px;">AI 正在分析中,请稍候...</p> |
|||
</div> |
|||
<div v-else-if="analysisData" class="drawer-content"> |
|||
<el-descriptions :column="1" border size="small"> |
|||
<el-descriptions-item label="姓名">{{ analysisData.employee_name }}</el-descriptions-item> |
|||
<el-descriptions-item label="部门">{{ analysisData.department || '-' }}</el-descriptions-item> |
|||
<el-descriptions-item label="分析周期">{{ analysisData.period || '-' }}</el-descriptions-item> |
|||
<el-descriptions-item label="任务完成率"> |
|||
<el-progress :percentage="Math.round((analysisData.task_completion_rate || 0) * 100)" :stroke-width="20" /> |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="活跃天数">{{ analysisData.active_days || 0 }} 天</el-descriptions-item> |
|||
<el-descriptions-item label="总交互次数">{{ analysisData.total_interactions || 0 }}</el-descriptions-item> |
|||
<el-descriptions-item label="效率趋势"> |
|||
<el-tag :type="trendTagType">{{ analysisData.efficiency_trend || '稳定' }}</el-tag> |
|||
<span style="margin-left: 8px; color: #909399; font-size: 12px;">{{ analysisData.efficiency_detail }}</span> |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="主要话题"> |
|||
<el-tag v-for="(t, i) in (analysisData.main_topics || [])" :key="i" size="small" style="margin-right: 4px;"> |
|||
{{ t }} |
|||
</el-tag> |
|||
<span v-if="!(analysisData.main_topics || []).length" style="color: #c0c4cc;">-</span> |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="优势特长"> |
|||
<ul style="margin: 0; padding-left: 16px;" v-if="(analysisData.strengths || []).length > 0"> |
|||
<li v-for="(s, i) in analysisData.strengths" :key="i" style="font-size: 13px;">{{ s }}</li> |
|||
</ul> |
|||
<span v-else style="color: #c0c4cc;">-</span> |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="成长建议"> |
|||
<ul style="margin: 0; padding-left: 16px;" v-if="(analysisData.growth_suggestions || []).length > 0"> |
|||
<li v-for="(s, i) in analysisData.growth_suggestions" :key="i" style="font-size: 13px;">{{ s }}</li> |
|||
</ul> |
|||
<span v-else style="color: #c0c4cc;">-</span> |
|||
</el-descriptions-item> |
|||
<el-descriptions-item label="人格特征">{{ analysisData.personality_traits || '-' }}</el-descriptions-item> |
|||
</el-descriptions> |
|||
</div> |
|||
</el-drawer> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted } from 'vue' |
|||
import { ref, computed } from 'vue' |
|||
import { Loading } from '@element-plus/icons-vue' |
|||
import { monitorApi } from '@/api' |
|||
|
|||
const loading = ref(false) |
|||
const employees = ref<any[]>([]) |
|||
const currentEmployee = ref<any>(null) |
|||
|
|||
onMounted(async () => { |
|||
const dashboardVisible = ref(false) |
|||
const dashboardLoading = ref(false) |
|||
const dashboardData = ref<any>(null) |
|||
|
|||
const analysisVisible = ref(false) |
|||
const analysisLoading = ref(false) |
|||
const analysisData = ref<any>(null) |
|||
|
|||
const breakdownList = computed(() => { |
|||
const map = dashboardData.value?.stats?.message_breakdown || {} |
|||
return Object.entries(map).map(([role, count]) => ({ role, count })) |
|||
}) |
|||
|
|||
const recentInteractions = computed(() => dashboardData.value?.stats?.recent_interactions || []) |
|||
|
|||
const trendTagType = computed(() => { |
|||
const t = analysisData.value?.efficiency_trend |
|||
if (t === '提升') return 'success' |
|||
if (t === '下降') return 'danger' |
|||
return 'warning' |
|||
}) |
|||
|
|||
async function loadEmployees() { |
|||
loading.value = true |
|||
try { |
|||
const res: any = await monitorApi.getEmployees() |
|||
employees.value = res || [] |
|||
employees.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
}) |
|||
} |
|||
|
|||
async function openDashboard(row: any) { |
|||
currentEmployee.value = row |
|||
dashboardVisible.value = true |
|||
dashboardLoading.value = true |
|||
dashboardData.value = null |
|||
try { |
|||
const res: any = await monitorApi.getDashboard(row.id) |
|||
dashboardData.value = res?.data || res |
|||
} finally { |
|||
dashboardLoading.value = false |
|||
} |
|||
} |
|||
|
|||
async function openAnalysis(row: any) { |
|||
currentEmployee.value = row |
|||
analysisVisible.value = true |
|||
analysisLoading.value = true |
|||
analysisData.value = null |
|||
try { |
|||
const res: any = await monitorApi.getAnalysis(row.id) |
|||
analysisData.value = res?.data || res |
|||
} finally { |
|||
analysisLoading.value = false |
|||
} |
|||
} |
|||
|
|||
loadEmployees() |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.drawer-content { padding: 0 4px; } |
|||
.stat-card { text-align: center; } |
|||
.stat-num { font-size: 28px; font-weight: 700; color: #409eff; } |
|||
.stat-label { font-size: 12px; color: #909399; margin-top: 4px; } |
|||
</style> |
|||
@ -0,0 +1,223 @@ |
|||
<template> |
|||
<div class="position-page"> |
|||
<el-card> |
|||
<template #header> |
|||
<div class="card-header"> |
|||
<span>用户岗位配置</span> |
|||
<el-tooltip content="岗位控制企业AI门户的使用权限和数据范围" placement="top"> |
|||
<el-icon style="color: #909399; margin-right: 8px;"><QuestionFilled /></el-icon> |
|||
</el-tooltip> |
|||
<el-button type="primary" @click="showAddDialog">新增岗位</el-button> |
|||
</div> |
|||
</template> |
|||
|
|||
<el-alert |
|||
type="info" |
|||
:closable="false" |
|||
style="margin-bottom: 16px;" |
|||
title="岗位决定了用户在企业AI门户中可以看到的功能和数据范围" |
|||
/> |
|||
|
|||
<el-table :data="positions" v-loading="loading"> |
|||
<el-table-column prop="name" label="岗位名称" width="160" /> |
|||
<el-table-column prop="code" label="岗位编码" width="140" /> |
|||
<el-table-column prop="description" label="描述" min-width="160" /> |
|||
<el-table-column label="类型" width="100"> |
|||
<template #default> |
|||
<el-tag type="warning" size="small">企业AI岗位</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="data_scope" label="数据范围" width="100"> |
|||
<template #default="{ row }"> |
|||
<el-tag>{{ dataScopeLabel(row.data_scope) }}</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="关联用户数" width="110"> |
|||
<template #default="{ row }"> |
|||
<span>{{ row.user_count || 0 }}</span> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作" width="250"> |
|||
<template #default="{ row }"> |
|||
<el-button size="small" @click="showPermissionDialog(row)">配置权限</el-button> |
|||
<el-button size="small" @click="showEditDialog(row)">编辑</el-button> |
|||
<el-button size="small" type="danger" :disabled="row.is_system" @click="handleDelete(row)">删除</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</el-card> |
|||
|
|||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑岗位' : '新增岗位'" width="500px"> |
|||
<el-form :model="form" label-width="80px"> |
|||
<el-form-item label="岗位名称"> |
|||
<el-input v-model="form.name" placeholder="如:高管、经理、普通员工" /> |
|||
</el-form-item> |
|||
<el-form-item label="岗位编码"> |
|||
<el-input v-model="form.code" :disabled="isEdit" placeholder="如:executive、manager" /> |
|||
</el-form-item> |
|||
<el-form-item label="描述"> |
|||
<el-input v-model="form.description" placeholder="岗位描述" /> |
|||
</el-form-item> |
|||
<el-form-item label="数据范围"> |
|||
<el-select v-model="form.data_scope" style="width: 100%;"> |
|||
<el-option label="仅自己" value="self_only"> |
|||
<span>仅自己</span> |
|||
<span style="color: #909399; font-size: 12px; margin-left: 8px;">只看自己的数据</span> |
|||
</el-option> |
|||
<el-option label="下属" value="subordinate_only"> |
|||
<span>下属</span> |
|||
<span style="color: #909399; font-size: 12px; margin-left: 8px;">查看自己和下属的数据</span> |
|||
</el-option> |
|||
<el-option label="全部" value="all"> |
|||
<span>全部</span> |
|||
<span style="color: #909399; font-size: 12px; margin-left: 8px;">查看全公司数据</span> |
|||
</el-option> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="dialogVisible = false">取消</el-button> |
|||
<el-button type="primary" @click="handleSubmit">确定</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
|
|||
<el-dialog v-model="permDialogVisible" title="配置岗位权限" width="600px"> |
|||
<el-alert |
|||
type="info" |
|||
:closable="false" |
|||
style="margin-bottom: 12px;" |
|||
title="以下权限控制该岗位在企业AI门户中可以使用的功能" |
|||
/> |
|||
<el-tree |
|||
:data="permTreeData" |
|||
show-checkbox |
|||
node-key="id" |
|||
:default-checked-keys="checkedPermIds" |
|||
:props="{ label: 'name' }" |
|||
ref="permTreeRef" |
|||
default-expand-all |
|||
/> |
|||
<template #footer> |
|||
<el-button @click="permDialogVisible = false">取消</el-button> |
|||
<el-button type="primary" @click="handlePermSubmit">保存</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted } from 'vue' |
|||
import { ElMessage, ElMessageBox } from 'element-plus' |
|||
import { QuestionFilled } from '@element-plus/icons-vue' |
|||
import { rbacApi } from '@/api' |
|||
|
|||
const loading = ref(false) |
|||
const positions = ref<any[]>([]) |
|||
const allPerms = ref<any[]>([]) |
|||
const dialogVisible = ref(false) |
|||
const isEdit = ref(false) |
|||
const editingPosition = ref<any>(null) |
|||
const form = ref<any>({ name: '', code: '', description: '', data_scope: 'self_only' }) |
|||
|
|||
const permDialogVisible = ref(false) |
|||
const permTreeRef = ref() |
|||
const checkedPermIds = ref<string[]>([]) |
|||
|
|||
const dataScopeLabels: Record<string, string> = { |
|||
all: '全部', |
|||
subordinate_only: '下属', |
|||
self_only: '仅自己', |
|||
} |
|||
function dataScopeLabel(s: string): string { |
|||
return dataScopeLabels[s] || s |
|||
} |
|||
|
|||
const permTreeData = ref<any[]>([]) |
|||
|
|||
onMounted(async () => { |
|||
await loadPositions() |
|||
await loadPerms() |
|||
}) |
|||
|
|||
async function loadPositions() { |
|||
loading.value = true |
|||
try { |
|||
const res: any = await rbacApi.getRoles() |
|||
positions.value = (res || []).filter((r: any) => r.role_type === 'position') |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
async function loadPerms() { |
|||
const res: any = await rbacApi.getPermissions() |
|||
allPerms.value = res || [] |
|||
permTreeData.value = res?.map((p: any) => ({ id: p.id, name: `${p.name} (${p.code})` })) || [] |
|||
} |
|||
|
|||
function showAddDialog() { |
|||
isEdit.value = false |
|||
editingPosition.value = null |
|||
form.value = { name: '', code: '', description: '', data_scope: 'self_only' } |
|||
dialogVisible.value = true |
|||
} |
|||
|
|||
function showEditDialog(row: any) { |
|||
isEdit.value = true |
|||
editingPosition.value = row |
|||
form.value = { name: row.name, code: row.code, description: row.description, data_scope: row.data_scope } |
|||
dialogVisible.value = true |
|||
} |
|||
|
|||
async function showPermissionDialog(row: any) { |
|||
editingPosition.value = row |
|||
const role: any = await rbacApi.getRole(row.id) |
|||
checkedPermIds.value = allPerms.value |
|||
.filter((p: any) => role.permissions?.includes(p.code)) |
|||
.map((p: any) => p.id) |
|||
permDialogVisible.value = true |
|||
} |
|||
|
|||
async function handleSubmit() { |
|||
try { |
|||
if (isEdit.value) { |
|||
await rbacApi.updateRole(editingPosition.value.id, form.value) |
|||
ElMessage.success('更新成功') |
|||
} else { |
|||
await rbacApi.createRole({ ...form.value, role_type: 'position' }) |
|||
ElMessage.success('创建成功') |
|||
} |
|||
dialogVisible.value = false |
|||
await loadPositions() |
|||
} catch { /* handled by interceptor */ } |
|||
} |
|||
|
|||
async function handlePermSubmit() { |
|||
const checked = permTreeRef.value?.getCheckedKeys() || [] |
|||
await rbacApi.updateRole(editingPosition.value.id, { permission_ids: checked }) |
|||
ElMessage.success('权限配置已保存') |
|||
permDialogVisible.value = false |
|||
await loadPositions() |
|||
} |
|||
|
|||
async function handleDelete(row: any) { |
|||
try { |
|||
await ElMessageBox.confirm('确认删除该岗位?', '提示', { type: 'warning' }) |
|||
await rbacApi.deleteRole(row.id) |
|||
ElMessage.success('删除成功') |
|||
await loadPositions() |
|||
} catch { /* user cancelled */ } |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.card-header { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
.card-header > span:first-child { |
|||
margin-right: auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,192 @@ |
|||
<template> |
|||
<div class="role-page"> |
|||
<el-card> |
|||
<template #header> |
|||
<div class="card-header"> |
|||
<span>平台角色管理</span> |
|||
<el-button type="primary" @click="showAddDialog">新增角色</el-button> |
|||
</div> |
|||
</template> |
|||
<el-table :data="roles" v-loading="loading"> |
|||
<el-table-column prop="name" label="角色名称" width="160" /> |
|||
<el-table-column prop="code" label="角色编码" width="140" /> |
|||
<el-table-column prop="description" label="描述" min-width="160" /> |
|||
<el-table-column prop="role_type" label="类型" width="100"> |
|||
<template #default> |
|||
<el-tag type="primary" size="small">平台角色</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="data_scope" label="数据范围" width="100"> |
|||
<template #default="{ row }"> |
|||
<el-tag>{{ dataScopeLabel(row.data_scope) }}</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column prop="is_system" label="来源" width="100"> |
|||
<template #default="{ row }"> |
|||
<el-tag :type="row.is_system ? 'info' : 'success'">{{ row.is_system ? '系统' : '自定义' }}</el-tag> |
|||
</template> |
|||
</el-table-column> |
|||
<el-table-column label="操作" width="250"> |
|||
<template #default="{ row }"> |
|||
<el-button size="small" @click="showPermissionDialog(row)">配置权限</el-button> |
|||
<el-button size="small" @click="showEditDialog(row)">编辑</el-button> |
|||
<el-button size="small" type="danger" :disabled="row.is_system" @click="handleDelete(row)">删除</el-button> |
|||
</template> |
|||
</el-table-column> |
|||
</el-table> |
|||
</el-card> |
|||
|
|||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑平台角色' : '新增平台角色'" width="500px"> |
|||
<el-form :model="form" label-width="80px"> |
|||
<el-form-item label="角色名称"> |
|||
<el-input v-model="form.name" placeholder="请输入角色名称" /> |
|||
</el-form-item> |
|||
<el-form-item label="编码"> |
|||
<el-input v-model="form.code" :disabled="isEdit" placeholder="角色编码(如 admin)" /> |
|||
</el-form-item> |
|||
<el-form-item label="描述"> |
|||
<el-input v-model="form.description" placeholder="角色描述" /> |
|||
</el-form-item> |
|||
<el-form-item label="数据范围"> |
|||
<el-select v-model="form.data_scope" style="width: 100%;"> |
|||
<el-option label="仅自己" value="self_only" /> |
|||
<el-option label="下属" value="subordinate_only" /> |
|||
<el-option label="全部" value="all" /> |
|||
</el-select> |
|||
</el-form-item> |
|||
</el-form> |
|||
<template #footer> |
|||
<el-button @click="dialogVisible = false">取消</el-button> |
|||
<el-button type="primary" @click="handleSubmit">确定</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
|
|||
<el-dialog v-model="permDialogVisible" title="配置权限" width="600px"> |
|||
<el-tree |
|||
:data="permTreeData" |
|||
show-checkbox |
|||
node-key="id" |
|||
:default-checked-keys="checkedPermIds" |
|||
:props="{ label: 'name' }" |
|||
ref="permTreeRef" |
|||
default-expand-all |
|||
/> |
|||
<template #footer> |
|||
<el-button @click="permDialogVisible = false">取消</el-button> |
|||
<el-button type="primary" @click="handlePermSubmit">保存</el-button> |
|||
</template> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script setup lang="ts"> |
|||
import { ref, onMounted } from 'vue' |
|||
import { ElMessage, ElMessageBox } from 'element-plus' |
|||
import { rbacApi } from '@/api' |
|||
|
|||
const loading = ref(false) |
|||
const roles = ref<any[]>([]) |
|||
const allPerms = ref<any[]>([]) |
|||
const dialogVisible = ref(false) |
|||
const isEdit = ref(false) |
|||
const editingRole = ref<any>(null) |
|||
const form = ref<any>({ name: '', code: '', description: '', data_scope: 'self_only' }) |
|||
|
|||
const permDialogVisible = ref(false) |
|||
const permTreeRef = ref() |
|||
const checkedPermIds = ref<string[]>([]) |
|||
|
|||
const dataScopeLabels: Record<string, string> = { |
|||
all: '全部', |
|||
subordinate_only: '下属', |
|||
self_only: '仅自己', |
|||
} |
|||
function dataScopeLabel(s: string): string { |
|||
return dataScopeLabels[s] || s |
|||
} |
|||
|
|||
const permTreeData = ref<any[]>([]) |
|||
|
|||
onMounted(async () => { |
|||
await loadRoles() |
|||
await loadPerms() |
|||
}) |
|||
|
|||
async function loadRoles() { |
|||
loading.value = true |
|||
try { |
|||
const res: any = await rbacApi.getRoles() |
|||
roles.value = (res || []).filter((r: any) => r.role_type === 'platform') |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
async function loadPerms() { |
|||
const res: any = await rbacApi.getPermissions() |
|||
allPerms.value = res || [] |
|||
permTreeData.value = res?.map((p: any) => ({ id: p.id, name: `${p.name} (${p.code})` })) || [] |
|||
} |
|||
|
|||
function showAddDialog() { |
|||
isEdit.value = false |
|||
editingRole.value = null |
|||
form.value = { name: '', code: '', description: '', data_scope: 'self_only' } |
|||
dialogVisible.value = true |
|||
} |
|||
|
|||
function showEditDialog(row: any) { |
|||
isEdit.value = true |
|||
editingRole.value = row |
|||
form.value = { name: row.name, code: row.code, description: row.description, data_scope: row.data_scope } |
|||
dialogVisible.value = true |
|||
} |
|||
|
|||
async function showPermissionDialog(row: any) { |
|||
editingRole.value = row |
|||
const role: any = await rbacApi.getRole(row.id) |
|||
checkedPermIds.value = allPerms.value |
|||
.filter((p: any) => role.permissions?.includes(p.code)) |
|||
.map((p: any) => p.id) |
|||
permDialogVisible.value = true |
|||
} |
|||
|
|||
async function handleSubmit() { |
|||
try { |
|||
if (isEdit.value) { |
|||
await rbacApi.updateRole(editingRole.value.id, form.value) |
|||
ElMessage.success('更新成功') |
|||
} else { |
|||
await rbacApi.createRole({ ...form.value, role_type: 'platform' }) |
|||
ElMessage.success('创建成功') |
|||
} |
|||
dialogVisible.value = false |
|||
await loadRoles() |
|||
} catch { /* handled by interceptor */ } |
|||
} |
|||
|
|||
async function handlePermSubmit() { |
|||
const checked = permTreeRef.value?.getCheckedKeys() || [] |
|||
await rbacApi.updateRole(editingRole.value.id, { permission_ids: checked }) |
|||
ElMessage.success('权限配置已保存') |
|||
permDialogVisible.value = false |
|||
await loadRoles() |
|||
} |
|||
|
|||
async function handleDelete(row: any) { |
|||
try { |
|||
await ElMessageBox.confirm('确认删除该平台角色?', '提示', { type: 'warning' }) |
|||
await rbacApi.deleteRole(row.id) |
|||
ElMessage.success('删除成功') |
|||
await loadRoles() |
|||
} catch { /* user cancelled */ } |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.card-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue