Browse Source

页面更新

master
MSI-7950X\刘泽明 2 hours ago
parent
commit
202f9b3431
  1. 3
      backend/database.py
  2. 17
      backend/dependencies.py
  3. 24
      backend/middleware/rbac_middleware.py
  4. 22
      backend/models/__init__.py
  5. 1
      backend/modules/auth/router.py
  6. 24
      backend/modules/rbac/router.py
  7. 21
      backend/modules/task/router.py
  8. 7
      backend/schemas/__init__.py
  9. 2
      frontend/src/api/index.ts
  10. 2
      frontend/src/components/common/PortalSwitcher.vue
  11. 93
      frontend/src/components/layout/AdminLayout.vue
  12. 243
      frontend/src/router/index.ts
  13. 71
      frontend/src/stores/user.ts
  14. 6
      frontend/src/views/chat/FlowChat.vue
  15. 2
      frontend/src/views/login/Login.vue
  16. 183
      frontend/src/views/monitor/EmployeeList.vue
  17. 19
      frontend/src/views/notification/NotificationCenter.vue
  18. 223
      frontend/src/views/rbac/PositionManage.vue
  19. 192
      frontend/src/views/rbac/RoleList.vue
  20. 52
      frontend/src/views/task/TaskList.vue

3
backend/database.py

@ -318,6 +318,9 @@ async def _run_migrations():
"created_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE memory_messages ADD COLUMN IF NOT EXISTS {col_sql}"))
await conn.execute(text(
"ALTER TABLE roles ADD COLUMN IF NOT EXISTS role_type VARCHAR(20) DEFAULT 'position'"
))
async def get_db():

17
backend/dependencies.py

@ -45,10 +45,16 @@ async def get_current_user(
)
roles = ur_result.scalars().all()
# 收集所有权限编码和数据权限范围
permissions = []
# 按类型分离:平台角色(管理后台)vs 岗位(企业AI)
platform_roles = []
position_roles = []
all_permissions = []
data_scopes = []
for role in roles:
if role.role_type == "platform":
platform_roles.append(role.code)
else:
position_roles.append(role.code)
data_scopes.append(role.data_scope)
rp_result = await db.execute(
select(Permission.code)
@ -56,15 +62,16 @@ async def get_current_user(
.where(RolePermission.role_id == role.id)
)
perms = rp_result.scalars().all()
permissions.extend(perms)
all_permissions.extend(perms)
return {
"id": str(user.id),
"username": user.username,
"display_name": user.display_name,
"department_id": str(user.department_id) if user.department_id else None,
"role": roles[0].code if roles else "employee",
"permissions": list(set(permissions)),
"platform_roles": list(set(platform_roles)),
"positions": list(set(position_roles)),
"permissions": list(set(all_permissions)),
"data_scope": "all" if "all" in data_scopes else (
"subordinate_only" if "subordinate_only" in data_scopes else "self_only"
),

24
backend/middleware/rbac_middleware.py

@ -59,13 +59,23 @@ async def rbac_middleware(request: Request, call_next):
)
roles = ur_result.scalars().all()
role_codes = [r.code for r in roles] # 角色编码列表
is_root = "root" in role_codes # 是否为超级管理员
# 按类型分离:平台角色(管理后台)vs 岗位(企业AI)
platform_roles = []
position_roles = []
is_root = False
# 收集所有权限编码和数据权限范围
permissions = []
data_scopes = []
for role in roles:
if role.role_type == "platform":
platform_roles.append({"code": role.code, "name": role.name, "data_scope": role.data_scope})
else:
position_roles.append({"code": role.code, "name": role.name, "data_scope": role.data_scope})
if role.code == "root":
is_root = True
data_scopes.append(role.data_scope)
rp_result = await db.execute(
select(Permission).join(RolePermission).where(RolePermission.role_id == role.id)
@ -73,9 +83,8 @@ async def rbac_middleware(request: Request, call_next):
perms = rp_result.scalars().all()
permissions.extend([p.code for p in perms])
unique_perms = list(set(permissions)) # 去重后的权限列表
unique_perms = list(set(permissions))
# 超级管理员自动拥有所有权限
if is_root and "*:*" not in unique_perms:
unique_perms.insert(0, "*:*")
@ -85,13 +94,12 @@ async def rbac_middleware(request: Request, call_next):
"username": user.username,
"display_name": user.display_name,
"department_id": str(user.department_id) if user.department_id else None,
"roles": [{"code": r.code, "name": r.name, "data_scope": r.data_scope} for r in roles],
"platform_roles": platform_roles,
"positions": position_roles,
"permissions": unique_perms,
"is_root": is_root,
"data_scope": "all" if is_root or "all" in data_scopes else (
"department" if "department" in data_scopes else
"subordinate_only" if "subordinate_only" in data_scopes else
"self_only"
"subordinate_only" if "subordinate_only" in data_scopes else "self_only"
),
}

22
backend/models/__init__.py

@ -53,18 +53,24 @@ class User(Base):
class Role(Base):
"""角色表 (roles),存储系统角色定义,用于 RBAC 权限管理。"""
"""角色/岗位表 (roles),存储系统角色和岗位定义,通过 role_type 区分。
role_type 有两种取值
- "platform": 管理后台平台角色超管系统管理员等
- "position": 企业AI用户岗位高管经理普通员工等
"""
__tablename__ = "roles"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 角色唯一标识 UUID
name = Column(String(50), unique=True, nullable=False) # 角色名称(唯一)
code = Column(String(50), unique=True, nullable=False, default="") # 角色编码(唯一,如 admin/user)
description = Column(String(200)) # 角色描述
is_system = Column(Boolean, default=False) # 是否为系统内置角色(不可删除)
data_scope = Column(String(50), default="self_only") # 数据权限范围:self_only/department/all
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 唯一标识 UUID
name = Column(String(50), unique=True, nullable=False) # 名称(唯一)
code = Column(String(50), unique=True, nullable=False, default="") # 编码(唯一,如 admin/user)
description = Column(String(200)) # 描述
role_type = Column(String(20), default="position") # 类型:platform(平台角色) / position(企业AI岗位)
is_system = Column(Boolean, default=False) # 是否为系统内置(不可删除)
data_scope = Column(String(50), default="self_only") # 数据权限范围:all/subordinate_only/self_only
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
permissions = relationship("RolePermission", back_populates="role") # 角色权限关联列表
permissions = relationship("RolePermission", back_populates="role") # 权限关联列表
class Permission(Base):

1
backend/modules/auth/router.py

@ -85,6 +85,7 @@ async def get_user_roles(db: AsyncSession, user_id: uuid.UUID) -> list[RoleOut]:
name=role.name,
code=role.code,
description=role.description,
role_type=role.role_type or "position",
is_system=role.is_system,
data_scope=role.data_scope,
permissions=perms,

24
backend/modules/rbac/router.py

@ -1,5 +1,5 @@
import uuid
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
@ -10,8 +10,16 @@ router = APIRouter(prefix="/api/rbac", tags=["rbac"])
@router.get("/roles", response_model=list[RoleOut])
async def get_roles(request: Request, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(Role))
async def get_roles(
request: Request,
role_type: str | None = Query(None, description="按类型过滤: platform(平台角色) / position(岗位)"),
db: AsyncSession = Depends(get_db),
):
"""获取角色列表,支持按 role_type 过滤。"""
stmt = select(Role)
if role_type:
stmt = stmt.where(Role.role_type == role_type)
result = await db.execute(stmt.order_by(Role.created_at))
roles = result.scalars().all()
return [await _role_to_out(db, r) for r in roles]
@ -30,7 +38,8 @@ async def get_role(role_id: uuid.UUID, request: Request, db: AsyncSession = Depe
async def create_role(req: RoleCreate, request: Request, db: AsyncSession = Depends(get_db)):
role = Role(
name=req.name, code=req.code or f"custom_{req.name}",
description=req.description, data_scope=req.data_scope,
description=req.description, role_type=req.role_type,
data_scope=req.data_scope,
)
db.add(role)
await db.flush()
@ -57,6 +66,8 @@ async def update_role(
role.name = req.name
if req.description is not None:
role.description = req.description
if req.role_type is not None:
role.role_type = req.role_type
if req.data_scope is not None:
role.data_scope = req.data_scope
@ -104,6 +115,7 @@ async def _role_to_out(db: AsyncSession, role: Role) -> RoleOut:
perms = list(rp_result.scalars().all())
return RoleOut(
id=role.id, name=role.name, code=role.code,
description=role.description, is_system=role.is_system,
data_scope=role.data_scope, permissions=perms,
description=role.description, role_type=role.role_type or "position",
is_system=role.is_system, data_scope=role.data_scope,
permissions=perms,
)

21
backend/modules/task/router.py

@ -1,5 +1,5 @@
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db
@ -11,11 +11,26 @@ router = APIRouter(prefix="/api/tasks", tags=["tasks"])
@router.get("", response_model=list[TaskOut])
async def get_tasks(request: Request, db: AsyncSession = Depends(get_db)):
async def get_tasks(
request: Request,
type: str | None = Query(None, description="筛选类型: published(我发布的) / received(我收到的)"),
db: AsyncSession = Depends(get_db),
):
user_ctx = request.state.user
cur_id = uuid.UUID(user_ctx["id"])
if user_ctx["data_scope"] == "all":
# 根据 type 参数确定查询范围
if type == "published":
# 我发布的任务
result = await db.execute(
select(Task).where(Task.assigner_id == cur_id)
)
elif type == "received":
# 我收到的任务
result = await db.execute(
select(Task).where(Task.assignee_id == cur_id)
)
elif user_ctx["data_scope"] == "all":
result = await db.execute(select(Task))
elif user_ctx["data_scope"] == "subordinate_only":
sub_ids = await _get_subordinate_ids(db, cur_id)

7
backend/schemas/__init__.py

@ -111,10 +111,11 @@ class DepartmentOut(BaseModel):
# --- Role ---
class RoleCreate(BaseModel):
"""创建角色请求体。"""
"""创建角色/岗位请求体。"""
name: str
code: str = ""
description: str | None = None
role_type: str = "position"
data_scope: str = "self_only"
permission_ids: list[uuid.UUID] = []
@ -123,16 +124,18 @@ class RoleUpdate(BaseModel):
"""更新角色请求体。"""
name: str | None = None
description: str | None = None
role_type: str | None = None
data_scope: str | None = None
permission_ids: list[uuid.UUID] | None = None
class RoleOut(BaseModel):
"""角色响应体,含权限编码列表。"""
"""角色/岗位响应体,含权限编码列表。"""
id: uuid.UUID
name: str
code: str = ""
description: str | None = None
role_type: str = "position"
is_system: bool
data_scope: str
permissions: list[str] = []

2
frontend/src/api/index.ts

@ -66,7 +66,7 @@ export const monitorApi = {
}
export const taskApi = {
getTasks: () => api.get('/tasks'),
getTasks: (type?: string) => api.get('/tasks', { params: { type } }),
createTask: (data: any) => api.post('/tasks', data),
getTask: (id: string) => api.get(`/tasks/${id}`),
updateTask: (id: string, data: any) => api.put(`/tasks/${id}`, data),

2
frontend/src/components/common/PortalSwitcher.vue

@ -35,7 +35,7 @@ const currentPortal = computed(() => {
function handleSwitch(portal: string) {
if (portal === 'user' && route.path.startsWith('/admin')) {
router.push('/user/dashboard')
router.push('/user/chat/flow')
} else if (portal === 'admin' && !route.path.startsWith('/admin')) {
router.push('/admin')
}

93
frontend/src/components/layout/AdminLayout.vue

@ -2,8 +2,8 @@
<el-container class="layout-container">
<el-aside :width="isCollapse ? '64px' : '220px'" class="layout-aside">
<div class="logo">
<span v-if="!isCollapse">管理后台</span>
<span v-else>M</span>
<span v-if="!isCollapse">{{ portalMode === 'admin' ? '管理后台' : '企业AI' }}</span>
<span v-else>{{ portalMode === 'admin' ? 'M' : 'E' }}</span>
</div>
<el-menu
:default-active="activeMenu"
@ -13,11 +13,37 @@
text-color="#bfcbd9"
active-text-color="#409EFF"
>
<el-menu-item index="/admin">
<el-icon><Monitor /></el-icon>
<span>控制台</span>
<!-- ======== 企业AI 门户菜单 ======== -->
<template v-if="portalMode === 'user'">
<el-menu-item index="/user/chat/flow" v-if="can('chat:use')">
<el-icon><ChatLineSquare /></el-icon>
<span>AI 助手</span>
</el-menu-item>
<el-sub-menu index="task" v-if="can('task:read')">
<template #title>
<el-icon><List /></el-icon>
<span>任务中心</span>
</template>
<el-menu-item index="/user/task/published">发布的任务</el-menu-item>
<el-menu-item index="/user/task/received">收到的任务</el-menu-item>
<el-menu-item index="/user/task/create" v-if="can('task:create')">创建任务</el-menu-item>
</el-sub-menu>
<el-menu-item index="/user/notification">
<el-icon><Bell /></el-icon>
<span>通知中心</span>
</el-menu-item>
<el-menu-item index="/user/monitor/employees" v-if="can('monitor:read')">
<el-icon><TrendCharts /></el-icon>
<span>员工效率</span>
</el-menu-item>
<el-menu-item index="/user/profile">
<el-icon><User /></el-icon>
<span>个人中心</span>
</el-menu-item>
</template>
<!-- ======== 管理后台菜单 ======== -->
<template v-else>
<el-sub-menu index="org" v-if="can('user:read')">
<template #title>
<el-icon><OfficeBuilding /></el-icon>
@ -27,12 +53,13 @@
<el-menu-item index="/admin/org/users">人员管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="role" v-if="can('role:read')">
<el-sub-menu index="rbac" v-if="can('role:read')">
<template #title>
<el-icon><Lock /></el-icon>
<span>权限管理</span>
</template>
<el-menu-item index="/admin/role/list">角色列表</el-menu-item>
<el-menu-item index="/admin/rbac/roles">平台角色</el-menu-item>
<el-menu-item index="/admin/positions/manage">用户岗位配置</el-menu-item>
</el-sub-menu>
<el-sub-menu index="flow" v-if="can('flow:read')">
@ -43,8 +70,9 @@
<el-menu-item index="/admin/flow/list">流列表</el-menu-item>
<el-menu-item index="/admin/flow/editor" v-if="can('flow:create')">流编辑器</el-menu-item>
<el-menu-item index="/admin/flow/market">流市场</el-menu-item>
<el-menu-item index="/admin/tools/custom" v-if="can('flow:create')">自定义API工具</el-menu-item>
<el-menu-item index="/admin/mcp/manager" v-if="can('flow:create')">MCP服务管理</el-menu-item>
<el-menu-item index="/admin/rag/knowledge">知识库管理</el-menu-item>
<el-menu-item index="/admin/custom-tools" v-if="can('flow:create')">自定义API工具</el-menu-item>
<el-menu-item index="/admin/mcp/servers" v-if="can('flow:create')">MCP服务管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="ai-config" v-if="can('flow:create')">
@ -53,28 +81,19 @@
<span>AI能力配置</span>
</template>
<el-menu-item index="/admin/model/providers">模型供应商管理</el-menu-item>
<el-menu-item index="/admin/rag/knowledge">知识库管理</el-menu-item>
<el-menu-item index="/admin/wecom/config">企微机器人配置</el-menu-item>
</el-sub-menu>
<el-sub-menu index="task-monitor" v-if="can('monitor:read')">
<template #title>
<el-icon><TrendCharts /></el-icon>
<span>任务与监控</span>
</template>
<el-menu-item index="/admin/task/create" v-if="can('task:create')">创建任务</el-menu-item>
<el-menu-item index="/admin/monitor/employees">员工监控</el-menu-item>
</el-sub-menu>
<el-sub-menu index="system">
<template #title>
<el-icon><Tools /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/admin/audit" v-if="can('audit:read')">审计日志</el-menu-item>
<el-menu-item index="/admin/system/monitor" v-if="can('audit:read')">系统监控</el-menu-item>
<el-menu-item index="/admin/settings" v-if="can('audit:read')">系统设置</el-menu-item>
<el-menu-item index="/admin/audit/logs" v-if="can('audit:read')">审计日志</el-menu-item>
<el-menu-item index="/admin/notification/manage" v-if="can('audit:read')">通知管理</el-menu-item>
<el-menu-item index="/admin/system/dashboard" v-if="can('audit:read')">系统监控</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</el-aside>
@ -83,7 +102,9 @@
<div class="header-left">
<el-button @click="isCollapse = !isCollapse" :icon="Fold" text />
<el-breadcrumb separator="/" style="margin-left: 16px">
<el-breadcrumb-item :to="{ path: '/admin' }">管理后台</el-breadcrumb-item>
<el-breadcrumb-item :to="{ path: portalMode === 'admin' ? '/admin' : '/user/chat/flow' }">
{{ portalMode === 'admin' ? '管理后台' : '企业AI' }}
</el-breadcrumb-item>
<el-breadcrumb-item v-if="route.meta.title">{{ route.meta.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
@ -115,7 +136,7 @@
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { Fold, User, ArrowDown, Monitor, OfficeBuilding, Lock, Share, Cpu, TrendCharts, Tools } from '@element-plus/icons-vue'
import { Fold, User, ArrowDown, OfficeBuilding, Lock, Share, Cpu, TrendCharts, Tools, ChatLineSquare, List, Bell } from '@element-plus/icons-vue'
import PortalSwitcher from '@/components/common/PortalSwitcher.vue'
const route = useRoute()
@ -123,31 +144,37 @@ const router = useRouter()
const userStore = useUserStore()
const isCollapse = ref(false)
const portalMode = computed(() => route.path.startsWith('/admin') ? 'admin' : 'user')
const activeMenu = computed(() => {
const path = route.path
if (portalMode.value === 'user') {
if (path.startsWith('/user/task/')) return 'task'
return path
}
if (path.startsWith('/admin/org')) return path
if (path.startsWith('/admin/role')) return path
if (path.startsWith('/admin/rbac')) return path
if (path.startsWith('/admin/positions')) return path
if (path.startsWith('/admin/flow')) return path
if (path.startsWith('/admin/tools')) return path
if (path.startsWith('/admin/custom-tools')) return path
if (path.startsWith('/admin/mcp')) return path
if (path.startsWith('/admin/rag')) return path
if (path.startsWith('/admin/agent')) return path
if (path.startsWith('/admin/wecom')) return path
if (path.startsWith('/admin/model')) return path
if (path.startsWith('/admin/task')) return path
if (path.startsWith('/admin/monitor')) return path
if (path.startsWith('/admin/audit')) return path
if (path.startsWith('/admin/notification')) return path
if (path.startsWith('/admin/system')) return path
if (path.startsWith('/admin/settings')) return path
return path
return '/admin'
})
function can(code: string): boolean {
return userStore.hasPermission(code)
return userStore.hasPermission(code, portalMode.value)
}
function handleCommand(cmd: string) {
if (cmd === 'logout') {
if (cmd === 'profile') {
router.push('/user/profile')
} else if (cmd === 'logout') {
userStore.logout()
router.push('/login')
}

243
frontend/src/router/index.ts

@ -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

71
frontend/src/stores/user.ts

@ -1,11 +1,14 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/api'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const user = ref<any>(JSON.parse(localStorage.getItem('user') || 'null'))
const permissions = ref<string[]>(JSON.parse(localStorage.getItem('permissions') || '[]'))
const platformPermissions = ref<string[]>(JSON.parse(localStorage.getItem('platformPermissions') || '[]'))
const positionPermissions = ref<string[]>(JSON.parse(localStorage.getItem('positionPermissions') || '[]'))
const isLoggedIn = computed(() => !!token.value)
const displayName = computed(() => user.value?.display_name || '')
@ -17,20 +20,36 @@ export const useUserStore = defineStore('user', () => {
return false
})
const isAdmin = computed(() => {
return isSuperAdmin.value || roleCodes.value.includes('super_admin')
if (isSuperAdmin.value) return true
const roles = user.value?.roles || []
return roles.some((r: any) => r.role_type === 'platform')
})
function setAuth(t: string, u: any) {
token.value = t
user.value = u
const perms = u?.roles?.flatMap((r: any) =>
(r.permissions || []).map((p: any) => typeof p === 'string' ? p : p.code)
) || []
permissions.value = perms
const posPerms = new Set<string>()
const platPerms = new Set<string>()
for (const role of (u?.roles || [])) {
const codes = (role.permissions || []).map((p: any) => typeof p === 'string' ? p : p.code)
if (role.role_type === 'platform') {
codes.forEach(c => platPerms.add(c))
} else {
codes.forEach(c => posPerms.add(c))
}
}
positionPermissions.value = [...posPerms]
platformPermissions.value = [...platPerms]
const all = [...new Set([...posPerms, ...platPerms])]
permissions.value = all
localStorage.setItem('token', t)
localStorage.setItem('user', JSON.stringify(u))
localStorage.setItem('permissions', JSON.stringify(perms))
localStorage.setItem('permissions', JSON.stringify(all))
localStorage.setItem('platformPermissions', JSON.stringify([...platPerms]))
localStorage.setItem('positionPermissions', JSON.stringify([...posPerms]))
}
async function fetchUser() {
@ -39,12 +58,27 @@ export const useUserStore = defineStore('user', () => {
const res = await api.get('/auth/me')
const u = res?.data || res || {}
user.value = u
const perms = u?.roles?.flatMap((r: any) =>
(r.permissions || []).map((p: any) => typeof p === 'string' ? p : p.code)
) || []
permissions.value = perms
const posPerms = new Set<string>()
const platPerms = new Set<string>()
for (const role of (u?.roles || [])) {
const codes = (role.permissions || []).map((p: any) => typeof p === 'string' ? p : p.code)
if (role.role_type === 'platform') {
codes.forEach(c => platPerms.add(c))
} else {
codes.forEach(c => posPerms.add(c))
}
}
positionPermissions.value = [...posPerms]
platformPermissions.value = [...platPerms]
const all = [...new Set([...posPerms, ...platPerms])]
permissions.value = all
localStorage.setItem('user', JSON.stringify(u))
localStorage.setItem('permissions', JSON.stringify(perms))
localStorage.setItem('permissions', JSON.stringify(all))
localStorage.setItem('platformPermissions', JSON.stringify([...platPerms]))
localStorage.setItem('positionPermissions', JSON.stringify([...posPerms]))
} catch {
logout()
}
@ -54,19 +88,28 @@ export const useUserStore = defineStore('user', () => {
token.value = ''
user.value = null
permissions.value = []
platformPermissions.value = []
positionPermissions.value = []
localStorage.removeItem('token')
localStorage.removeItem('user')
localStorage.removeItem('permissions')
localStorage.removeItem('platformPermissions')
localStorage.removeItem('positionPermissions')
}
function hasPermission(code: string): boolean {
function hasPermission(code: string, portal?: 'admin' | 'user'): boolean {
if (isSuperAdmin.value) return true
if (roleCodes.value.includes('super_admin')) return true
if (portal === 'admin') {
return code === '*' || platformPermissions.value.includes(code)
}
if (portal === 'user') {
return code === '*' || positionPermissions.value.includes(code)
}
return permissions.value.includes(code)
}
return {
token, user, permissions,
token, user, permissions, platformPermissions, positionPermissions,
isLoggedIn, displayName, username, roleCodes, isSuperAdmin, isAdmin,
setAuth, fetchUser, logout, hasPermission,
}

6
frontend/src/views/chat/FlowChat.vue

@ -1,10 +1,8 @@
<template>
<div class="flow-chat-page">
<el-page-header @back="$router.back()" content="流式对话" />
<el-card style="margin-top: 20px">
<el-card>
<el-form inline>
<el-form-item label="选择">
<el-form-item label="选择智能体">
<el-select v-model="selectedFlowId" placeholder="选择已发布的流" filterable @change="onFlowChange">
<el-option v-for="f in publishedFlows" :key="f.id" :label="f.name" :value="f.id" />
</el-select>

2
frontend/src/views/login/Login.vue

@ -56,7 +56,7 @@ async function handleLogin() {
const res: any = await authApi.login({ username: form.username, password: form.password })
userStore.setAuth(res.access_token, res.user)
const redirect = (route.query.redirect as string) || ''
const targetPath = redirect || '/user/dashboard'
const targetPath = redirect || '/user/chat/flow'
router.push(targetPath)
} finally {
loading.value = false

183
frontend/src/views/monitor/EmployeeList.vue

@ -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>

19
frontend/src/views/notification/NotificationCenter.vue

@ -3,8 +3,8 @@
<el-card>
<template #header>
<div class="card-header">
<span>通知中心</span>
<div>
<span>{{ isAdminMode ? '通知管理' : '通知中心' }}</span>
<div v-if="!isAdminMode">
<el-tag style="margin-right: 12px" :type="wsConnected ? 'success' : 'info'">
{{ wsConnected ? '已连接' : '未连接' }}
</el-tag>
@ -16,7 +16,7 @@
</template>
<el-tabs v-model="activeTab">
<el-tab-pane label="实时消息" name="messages">
<el-tab-pane v-if="!isAdminMode" label="实时消息" name="messages">
<div class="message-list" ref="msgListRef">
<div v-for="(msg, i) in messages" :key="i" :class="['msg-item', 'msg-' + (msg.type || 'info')]">
<div class="msg-header">
@ -29,7 +29,7 @@
</div>
</el-tab-pane>
<el-tab-pane label="发送通知" name="send">
<el-tab-pane v-if="isAdminMode" label="发送通知" name="send">
<el-form :model="sendForm" label-width="80px" style="max-width: 500px">
<el-form-item label="标题">
<el-input v-model="sendForm.title" placeholder="通知标题" />
@ -49,7 +49,7 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="模板管理" name="templates">
<el-tab-pane v-if="isAdminMode" label="模板管理" name="templates">
<el-table :data="templates">
<el-table-column prop="name" label="名称" />
<el-table-column prop="code" label="编码" />
@ -67,13 +67,16 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { notificationApi } from '@/api'
import { useUserStore } from '@/stores/user'
const route = useRoute()
const userStore = useUserStore()
const activeTab = ref('messages')
const isAdminMode = computed(() => route.path.startsWith('/admin'))
const activeTab = ref(isAdminMode.value ? 'send' : 'messages')
const wsConnected = ref(false)
const messages = ref<any[]>([])
const sending = ref(false)
@ -137,7 +140,7 @@ async function deleteTemplate(row: any) {
}
onMounted(() => {
connectWs()
if (!isAdminMode.value) connectWs()
loadTemplates()
})
onUnmounted(() => disconnectWs())

223
frontend/src/views/rbac/PositionManage.vue

@ -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>

192
frontend/src/views/rbac/RoleList.vue

@ -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>

52
frontend/src/views/task/TaskList.vue

@ -3,8 +3,7 @@
<el-card>
<template #header>
<div class="card-header">
<span>任务列表</span>
<el-button type="primary" @click="$router.push('/admin/task/create')" v-if="userStore.hasPermission('task:create')">创建任务</el-button>
<span>{{ pageTitle }}</span>
</div>
</template>
@ -23,12 +22,17 @@
<el-table-column prop="deadline" label="截止日期" width="180">
<template #default="{ row }">{{ row.deadline ? new Date(row.deadline).toLocaleDateString() : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="260">
<el-table-column label="操作" width="160">
<template #default="{ row }">
<el-button size="small" @click="$router.push(`/user/task/${row.id}`)">详情</el-button>
<el-button size="small" type="primary" @click="editTask(row)" v-if="userStore.hasPermission('task:create')">编辑</el-button>
<el-button size="small" type="warning" @click="handlePush(row)">推送企微</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)" v-if="userStore.hasPermission('task:create')">删除</el-button>
<el-button
v-if="mode === 'published' && userStore.hasPermission('task:create')"
size="small"
type="danger"
@click="handleDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
@ -37,25 +41,19 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { taskApi } from '@/api'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const loading = ref(false)
const tasks = ref<any[]>([])
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确认删除任务 "${row.title}"?`, '提示', { type: 'warning' })
await taskApi.deleteTask(row.id)
ElMessage.success('已删除')
tasks.value = tasks.value.filter((t: any) => t.id !== row.id)
} catch { /**/ }
}
const mode = computed(() => route.path.includes('/received') ? 'received' : 'published')
const pageTitle = computed(() => mode.value === 'published' ? '发布的任务' : '收到的任务')
const statusLabel = (s: string) => ({ pending: '待处理', in_progress: '进行中', completed: '已完成', cancelled: '已取消' } as any)[s] || s
const statusType = (s: string) => ({ pending: 'info', in_progress: 'warning', completed: 'success', cancelled: 'danger' } as any)[s] || 'info'
@ -63,22 +61,26 @@ const priorityLabel = (p: string) => ({ low: '低', normal: '中', high: '高',
const priorityType = (p: string) => ({ low: 'info', normal: '', high: 'warning', urgent: 'danger' } as any)[p] || ''
onMounted(async () => {
await loadTasks()
})
async function loadTasks() {
loading.value = true
try {
const res: any = await taskApi.getTasks()
const res: any = await taskApi.getTasks(mode.value)
tasks.value = res || []
} finally {
loading.value = false
}
})
function editTask(row: any) {
router.push(`/admin/task/create?edit=${row.id}`)
}
async function handlePush(row: any) {
await taskApi.pushTask(row.id)
ElMessage.success('已推送')
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确认删除任务 "${row.title}"?`, '提示', { type: 'warning' })
await taskApi.deleteTask(row.id)
ElMessage.success('已删除')
tasks.value = tasks.value.filter((t: any) => t.id !== row.id)
} catch { /**/ }
}
</script>

Loading…
Cancel
Save