Browse Source

页面更新

master
MSI-7950X\刘泽明 3 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()", "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(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(): async def get_db():

17
backend/dependencies.py

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

24
backend/middleware/rbac_middleware.py

@ -59,13 +59,23 @@ async def rbac_middleware(request: Request, call_next):
) )
roles = ur_result.scalars().all() roles = ur_result.scalars().all()
role_codes = [r.code for r in roles] # 角色编码列表 # 按类型分离:平台角色(管理后台)vs 岗位(企业AI)
is_root = "root" in role_codes # 是否为超级管理员 platform_roles = []
position_roles = []
is_root = False
# 收集所有权限编码和数据权限范围 # 收集所有权限编码和数据权限范围
permissions = [] permissions = []
data_scopes = [] data_scopes = []
for role in roles: 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) data_scopes.append(role.data_scope)
rp_result = await db.execute( rp_result = await db.execute(
select(Permission).join(RolePermission).where(RolePermission.role_id == role.id) 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() perms = rp_result.scalars().all()
permissions.extend([p.code for p in perms]) 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: if is_root and "*:*" not in unique_perms:
unique_perms.insert(0, "*:*") unique_perms.insert(0, "*:*")
@ -85,13 +94,12 @@ async def rbac_middleware(request: Request, call_next):
"username": user.username, "username": user.username,
"display_name": user.display_name, "display_name": user.display_name,
"department_id": str(user.department_id) if user.department_id else None, "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, "permissions": unique_perms,
"is_root": is_root, "is_root": is_root,
"data_scope": "all" if is_root or "all" in data_scopes else ( "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): class Role(Base):
"""角色表 (roles),存储系统角色定义,用于 RBAC 权限管理。""" """角色/岗位表 (roles),存储系统角色和岗位定义,通过 role_type 区分。
role_type 有两种取值
- "platform": 管理后台平台角色超管系统管理员等
- "position": 企业AI用户岗位高管经理普通员工等
"""
__tablename__ = "roles" __tablename__ = "roles"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 角色唯一标识 UUID id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 唯一标识 UUID
name = Column(String(50), unique=True, nullable=False) # 角色名称(唯一) name = Column(String(50), unique=True, nullable=False) # 名称(唯一)
code = Column(String(50), unique=True, nullable=False, default="") # 角色编码(唯一,如 admin/user) code = Column(String(50), unique=True, nullable=False, default="") # 编码(唯一,如 admin/user)
description = Column(String(200)) # 角色描述 description = Column(String(200)) # 描述
is_system = Column(Boolean, default=False) # 是否为系统内置角色(不可删除) role_type = Column(String(20), default="position") # 类型:platform(平台角色) / position(企业AI岗位)
data_scope = Column(String(50), default="self_only") # 数据权限范围:self_only/department/all 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) # 记录创建时间 created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
permissions = relationship("RolePermission", back_populates="role") # 角色权限关联列表 permissions = relationship("RolePermission", back_populates="role") # 权限关联列表
class Permission(Base): 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, name=role.name,
code=role.code, code=role.code,
description=role.description, description=role.description,
role_type=role.role_type or "position",
is_system=role.is_system, is_system=role.is_system,
data_scope=role.data_scope, data_scope=role.data_scope,
permissions=perms, permissions=perms,

24
backend/modules/rbac/router.py

@ -1,5 +1,5 @@
import uuid import uuid
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Query, Request
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db from database import get_db
@ -10,8 +10,16 @@ router = APIRouter(prefix="/api/rbac", tags=["rbac"])
@router.get("/roles", response_model=list[RoleOut]) @router.get("/roles", response_model=list[RoleOut])
async def get_roles(request: Request, db: AsyncSession = Depends(get_db)): async def get_roles(
result = await db.execute(select(Role)) 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() roles = result.scalars().all()
return [await _role_to_out(db, r) for r in roles] 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)): async def create_role(req: RoleCreate, request: Request, db: AsyncSession = Depends(get_db)):
role = Role( role = Role(
name=req.name, code=req.code or f"custom_{req.name}", 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) db.add(role)
await db.flush() await db.flush()
@ -57,6 +66,8 @@ async def update_role(
role.name = req.name role.name = req.name
if req.description is not None: if req.description is not None:
role.description = req.description role.description = req.description
if req.role_type is not None:
role.role_type = req.role_type
if req.data_scope is not None: if req.data_scope is not None:
role.data_scope = req.data_scope 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()) perms = list(rp_result.scalars().all())
return RoleOut( return RoleOut(
id=role.id, name=role.name, code=role.code, id=role.id, name=role.name, code=role.code,
description=role.description, is_system=role.is_system, description=role.description, role_type=role.role_type or "position",
data_scope=role.data_scope, permissions=perms, is_system=role.is_system, data_scope=role.data_scope,
permissions=perms,
) )

21
backend/modules/task/router.py

@ -1,5 +1,5 @@
import uuid import uuid
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db from database import get_db
@ -11,11 +11,26 @@ router = APIRouter(prefix="/api/tasks", tags=["tasks"])
@router.get("", response_model=list[TaskOut]) @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 user_ctx = request.state.user
cur_id = uuid.UUID(user_ctx["id"]) 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)) result = await db.execute(select(Task))
elif user_ctx["data_scope"] == "subordinate_only": elif user_ctx["data_scope"] == "subordinate_only":
sub_ids = await _get_subordinate_ids(db, cur_id) sub_ids = await _get_subordinate_ids(db, cur_id)

7
backend/schemas/__init__.py

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

2
frontend/src/api/index.ts

@ -66,7 +66,7 @@ export const monitorApi = {
} }
export const taskApi = { export const taskApi = {
getTasks: () => api.get('/tasks'), getTasks: (type?: string) => api.get('/tasks', { params: { type } }),
createTask: (data: any) => api.post('/tasks', data), createTask: (data: any) => api.post('/tasks', data),
getTask: (id: string) => api.get(`/tasks/${id}`), getTask: (id: string) => api.get(`/tasks/${id}`),
updateTask: (id: string, data: any) => api.put(`/tasks/${id}`, data), 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) { function handleSwitch(portal: string) {
if (portal === 'user' && route.path.startsWith('/admin')) { if (portal === 'user' && route.path.startsWith('/admin')) {
router.push('/user/dashboard') router.push('/user/chat/flow')
} else if (portal === 'admin' && !route.path.startsWith('/admin')) { } else if (portal === 'admin' && !route.path.startsWith('/admin')) {
router.push('/admin') router.push('/admin')
} }

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

@ -2,8 +2,8 @@
<el-container class="layout-container"> <el-container class="layout-container">
<el-aside :width="isCollapse ? '64px' : '220px'" class="layout-aside"> <el-aside :width="isCollapse ? '64px' : '220px'" class="layout-aside">
<div class="logo"> <div class="logo">
<span v-if="!isCollapse">管理后台</span> <span v-if="!isCollapse">{{ portalMode === 'admin' ? '管理后台' : '企业AI' }}</span>
<span v-else>M</span> <span v-else>{{ portalMode === 'admin' ? 'M' : 'E' }}</span>
</div> </div>
<el-menu <el-menu
:default-active="activeMenu" :default-active="activeMenu"
@ -13,11 +13,37 @@
text-color="#bfcbd9" text-color="#bfcbd9"
active-text-color="#409EFF" active-text-color="#409EFF"
> >
<el-menu-item index="/admin"> <!-- ======== 企业AI 门户菜单 ======== -->
<el-icon><Monitor /></el-icon> <template v-if="portalMode === 'user'">
<span>控制台</span> <el-menu-item index="/user/chat/flow" v-if="can('chat:use')">
<el-icon><ChatLineSquare /></el-icon>
<span>AI 助手</span>
</el-menu-item> </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')"> <el-sub-menu index="org" v-if="can('user:read')">
<template #title> <template #title>
<el-icon><OfficeBuilding /></el-icon> <el-icon><OfficeBuilding /></el-icon>
@ -27,12 +53,13 @@
<el-menu-item index="/admin/org/users">人员管理</el-menu-item> <el-menu-item index="/admin/org/users">人员管理</el-menu-item>
</el-sub-menu> </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> <template #title>
<el-icon><Lock /></el-icon> <el-icon><Lock /></el-icon>
<span>权限管理</span> <span>权限管理</span>
</template> </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>
<el-sub-menu index="flow" v-if="can('flow:read')"> <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/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/editor" v-if="can('flow:create')">流编辑器</el-menu-item>
<el-menu-item index="/admin/flow/market">流市场</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/rag/knowledge">知识库管理</el-menu-item>
<el-menu-item index="/admin/mcp/manager" v-if="can('flow:create')">MCP服务管理</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>
<el-sub-menu index="ai-config" v-if="can('flow:create')"> <el-sub-menu index="ai-config" v-if="can('flow:create')">
@ -53,28 +81,19 @@
<span>AI能力配置</span> <span>AI能力配置</span>
</template> </template>
<el-menu-item index="/admin/model/providers">模型供应商管理</el-menu-item> <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-menu-item index="/admin/wecom/config">企微机器人配置</el-menu-item>
</el-sub-menu> </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"> <el-sub-menu index="system">
<template #title> <template #title>
<el-icon><Tools /></el-icon> <el-icon><Tools /></el-icon>
<span>系统管理</span> <span>系统管理</span>
</template> </template>
<el-menu-item index="/admin/audit" 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/system/monitor" 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/settings" 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> </el-sub-menu>
</template>
</el-menu> </el-menu>
</el-aside> </el-aside>
@ -83,7 +102,9 @@
<div class="header-left"> <div class="header-left">
<el-button @click="isCollapse = !isCollapse" :icon="Fold" text /> <el-button @click="isCollapse = !isCollapse" :icon="Fold" text />
<el-breadcrumb separator="/" style="margin-left: 16px"> <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-item v-if="route.meta.title">{{ route.meta.title }}</el-breadcrumb-item>
</el-breadcrumb> </el-breadcrumb>
</div> </div>
@ -115,7 +136,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user' 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' import PortalSwitcher from '@/components/common/PortalSwitcher.vue'
const route = useRoute() const route = useRoute()
@ -123,31 +144,37 @@ const router = useRouter()
const userStore = useUserStore() const userStore = useUserStore()
const isCollapse = ref(false) const isCollapse = ref(false)
const portalMode = computed(() => route.path.startsWith('/admin') ? 'admin' : 'user')
const activeMenu = computed(() => { const activeMenu = computed(() => {
const path = route.path 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/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/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/mcp')) return path
if (path.startsWith('/admin/rag')) 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/wecom')) return path
if (path.startsWith('/admin/model')) 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/audit')) return path
if (path.startsWith('/admin/notification')) return path
if (path.startsWith('/admin/system')) return path if (path.startsWith('/admin/system')) return path
if (path.startsWith('/admin/settings')) return path return '/admin'
return path
}) })
function can(code: string): boolean { function can(code: string): boolean {
return userStore.hasPermission(code) return userStore.hasPermission(code, portalMode.value)
} }
function handleCommand(cmd: string) { function handleCommand(cmd: string) {
if (cmd === 'logout') { if (cmd === 'profile') {
router.push('/user/profile')
} else if (cmd === 'logout') {
userStore.logout() userStore.logout()
router.push('/login') router.push('/login')
} }

243
frontend/src/router/index.ts

@ -1,253 +1,192 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: [ routes: [
{ {
path: '/', path: '/',
redirect: '/login', redirect: '/user/chat/flow',
},
{
path: '/:pathMatch(.*)*',
redirect: '/login',
}, },
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
component: () => import('@/views/login/Login.vue'), component: () => import('@/views/login/Login.vue'),
}, },
// ========== 企业AI 门户 ==========
{ {
path: '/user', path: '/user',
component: () => import('@/components/layout/MainLayout.vue'), component: () => import('@/components/layout/AdminLayout.vue'),
redirect: '/user/dashboard',
children: [ children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/Dashboard.vue'),
meta: { title: '工作台' },
},
{ {
path: 'chat/flow', path: 'chat/flow',
name: 'FlowChat', name: 'UserAIHelper',
component: () => import('@/views/chat/FlowChat.vue'), component: () => import('@/views/chat/FlowChat.vue'),
meta: { title: '流式对话' }, meta: { title: 'AI 助手' },
}, },
{ {
path: 'agent/list', path: 'task/published',
name: 'AgentList', name: 'UserTaskPublished',
component: () => import('@/views/agent/AgentList.vue'), component: () => import('@/views/task/TaskList.vue'),
meta: { title: '智能体' }, meta: { title: '发布的任务' },
},
{
path: 'agent/chat/:type',
name: 'AgentChat',
component: () => import('@/views/agent/AgentChat.vue'),
meta: { title: '智能体对话' },
}, },
{ {
path: 'document/manager', path: 'task/received',
name: 'DocumentManager', name: 'UserTaskReceived',
component: () => import('@/views/document/DocumentManager.vue'), component: () => import('@/views/task/TaskList.vue'),
meta: { title: '文档管理' }, meta: { title: '收到的任务' },
}, },
{ {
path: 'task/list', path: 'task/create',
name: 'TaskList', name: 'UserTaskCreate',
component: () => import('@/views/task/TaskList.vue'), component: () => import('@/views/task/TaskCreate.vue'),
meta: { title: '任务列表', perms: ['task:read'] }, meta: { title: '创建任务' },
}, },
{ {
path: 'task/:id', path: 'task/:id',
name: 'TaskDetail', name: 'UserTaskDetail',
component: () => import('@/views/task/TaskDetail.vue'), component: () => import('@/views/task/TaskDetail.vue'),
meta: { title: '任务详情', perms: ['task:read'] }, meta: { title: '任务详情' },
}, },
{ {
path: 'notification/center', path: 'notification',
name: 'NotificationCenter', name: 'UserNotification',
component: () => import('@/views/notification/NotificationCenter.vue'), component: () => import('@/views/notification/NotificationCenter.vue'),
meta: { title: '通知中心' }, meta: { title: '通知中心' },
}, },
{
path: 'monitor/employees',
name: 'UserMonitorEmployees',
component: () => import('@/views/monitor/EmployeeList.vue'),
meta: { title: '员工效率' },
},
{ {
path: 'profile', path: 'profile',
name: 'Profile', name: 'UserProfile',
component: () => import('@/views/profile/Profile.vue'), component: () => import('@/views/profile/Profile.vue'),
meta: { title: '个人中心' }, meta: { title: '个人中心' },
}, },
{
path: ':pathMatch(.*)*',
redirect: '/user/chat/flow',
},
], ],
}, },
// ========== 管理后台 ==========
{ {
path: '/admin', path: '/admin',
component: () => import('@/components/layout/AdminLayout.vue'), component: () => import('@/components/layout/AdminLayout.vue'),
redirect: '/admin',
children: [ children: [
{ {
path: '', path: '',
name: 'AdminDashboard', redirect: '/admin/model/providers',
component: () => import('@/views/dashboard/Dashboard.vue'),
meta: { title: '控制台', perms: ['admin:access'] },
}, },
// ---- 系统管理 ----
{ {
path: 'org/departments', path: 'model/providers',
name: 'AdminDepartments', name: 'AdminModelProviders',
component: () => import('@/views/org/DepartmentTree.vue'), component: () => import('@/views/model/ModelProviderManager.vue'),
meta: { title: '部门管理', perms: ['user:read'] }, meta: { title: '模型管理' },
}, },
{ {
path: 'org/users', path: 'rbac/roles',
name: 'AdminUserList', name: 'AdminRbacRoles',
component: () => import('@/views/org/UserList.vue'), component: () => import('@/views/rbac/RoleList.vue'),
meta: { title: '人员管理', perms: ['user:read'] }, meta: { title: '平台角色' },
}, },
{ {
path: 'role/list', path: 'positions/manage',
name: 'AdminRoleList', name: 'AdminPositionsManage',
component: () => import('@/views/role/RoleList.vue'), component: () => import('@/views/rbac/PositionManage.vue'),
meta: { title: '角色管理', perms: ['role:read'] }, meta: { title: '用户岗位配置' },
}, },
{ {
path: 'role/:id/permissions', path: 'org/departments',
name: 'AdminRolePermissions', name: 'AdminOrgDepartments',
component: () => import('@/views/role/PermissionConfig.vue'), component: () => import('@/views/org/DepartmentTree.vue'),
meta: { title: '权限配置', perms: ['role:read'] }, meta: { title: '组织架构' },
}, },
{ {
path: 'flow/list', path: 'org/users',
name: 'AdminFlowList', name: 'AdminOrgUsers',
component: () => import('@/views/flow/FlowList.vue'), component: () => import('@/views/org/UserList.vue'),
meta: { title: '流列表', perms: ['flow:read'] }, meta: { title: '人员管理' },
}, },
// ---- AI 应用 ----
{ {
path: 'flow/editor', path: 'flow/editor',
name: 'AdminFlowEditor', name: 'AdminFlowEditor',
component: () => import('@/views/flow/FlowEditor.vue'), component: () => import('@/views/flow/FlowEditor.vue'),
meta: { title: '流编辑器', perms: ['flow:create'] }, meta: { title: '流编辑器' },
}, },
{ {
path: 'flow/editor/:id', path: 'flow/editor/:id',
name: 'AdminFlowEditorEdit', name: 'AdminFlowEditorEdit',
component: () => import('@/views/flow/FlowEditor.vue'), 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', path: 'flow/market',
name: 'AdminFlowMarket', name: 'AdminFlowMarket',
component: () => import('@/views/flow/FlowMarket.vue'), component: () => import('@/views/flow/FlowMarket.vue'),
meta: { title: '流市场', perms: ['flow:read'] }, meta: { title: '流市场' },
}, },
{ {
path: 'flow/market/:id', path: 'rag/knowledge',
name: 'AdminFlowDetail', name: 'AdminRagKnowledge',
component: () => import('@/views/flow/FlowDetail.vue'), component: () => import('@/views/rag/KnowledgeBase.vue'),
meta: { title: '流详情', perms: ['flow:read'] }, meta: { title: '知识库管理' },
}, },
// ---- 工具集成 ----
{ {
path: 'tools/custom', path: 'custom-tools',
name: 'AdminCustomToolManager', name: 'AdminCustomTools',
component: () => import('@/views/tools/CustomToolManager.vue'), component: () => import('@/views/tools/CustomToolManager.vue'),
meta: { title: '自定义API工具', perms: ['flow:create'] }, meta: { title: '自定义工具' },
}, },
{ {
path: 'mcp/manager', path: 'mcp/servers',
name: 'AdminMcpManager', name: 'AdminMcpServers',
component: () => import('@/views/mcp/McpManager.vue'), component: () => import('@/views/mcp/McpManager.vue'),
meta: { title: 'MCP服务管理', perms: ['flow:create'] }, meta: { title: 'MCP 服务注册' },
},
{
path: 'rag/knowledge',
name: 'AdminKnowledgeBase',
component: () => import('@/views/rag/KnowledgeBase.vue'),
meta: { title: '知识库管理', perms: ['flow:create'] },
}, },
{ {
path: 'wecom/config', path: 'wecom/config',
name: 'AdminWecomConfig', name: 'AdminWecomConfig',
component: () => import('@/views/wecom/BotConfig.vue'), component: () => import('@/views/wecom/BotConfig.vue'),
meta: { title: '企微机器人配置', perms: ['admin:access'] }, meta: { title: '企微机器人配置' },
},
{
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'] },
}, },
// ---- 审计日志 ----
{ {
path: 'task/create', path: 'audit/logs',
name: 'AdminTaskCreate', name: 'AdminAuditLogs',
component: () => import('@/views/task/TaskCreate.vue'), component: () => import('@/views/audit/AuditLog.vue'),
meta: { title: '创建任务', perms: ['task:create'] }, meta: { title: '操作日志' },
}, },
{ {
path: 'audit', path: 'notification/manage',
name: 'AdminAudit', name: 'AdminNotificationManage',
component: () => import('@/views/audit/AuditLog.vue'), component: () => import('@/views/notification/NotificationCenter.vue'),
meta: { title: '审计日志', perms: ['audit:read'] }, meta: { title: '通知管理' },
}, },
{ {
path: 'system/monitor', path: 'system/dashboard',
name: 'AdminSystemMonitor', name: 'AdminSystemDashboard',
component: () => import('@/views/system/SystemMonitor.vue'), component: () => import('@/views/system/SystemMonitor.vue'),
meta: { title: '系统监控', perms: ['audit:read'] }, meta: { title: '系统监控' },
}, },
{ {
path: 'settings', path: ':pathMatch(.*)*',
name: 'AdminSettings', redirect: '/admin/model/providers',
component: () => import('@/views/settings/Settings.vue'),
meta: { title: '系统设置', perms: ['audit:read'] },
}, },
], ],
}, },
], ],
}) })
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 export default router

71
frontend/src/stores/user.ts

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

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

@ -1,10 +1,8 @@
<template> <template>
<div class="flow-chat-page"> <div class="flow-chat-page">
<el-page-header @back="$router.back()" content="流式对话" /> <el-card>
<el-card style="margin-top: 20px">
<el-form inline> <el-form inline>
<el-form-item label="选择"> <el-form-item label="选择智能体">
<el-select v-model="selectedFlowId" placeholder="选择已发布的流" filterable @change="onFlowChange"> <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-option v-for="f in publishedFlows" :key="f.id" :label="f.name" :value="f.id" />
</el-select> </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 }) const res: any = await authApi.login({ username: form.username, password: form.password })
userStore.setAuth(res.access_token, res.user) userStore.setAuth(res.access_token, res.user)
const redirect = (route.query.redirect as string) || '' const redirect = (route.query.redirect as string) || ''
const targetPath = redirect || '/user/dashboard' const targetPath = redirect || '/user/chat/flow'
router.push(targetPath) router.push(targetPath)
} finally { } finally {
loading.value = false loading.value = false

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

@ -1,38 +1,199 @@
<template> <template>
<div class="monitor-page"> <div>
<el-card> <el-card>
<template #header> <template #header>
<span>员工工作监控</span> <span>员工工作监控</span>
</template> </template>
<el-table :data="employees" v-loading="loading"> <el-table :data="employees" v-loading="loading">
<el-table-column prop="display_name" label="姓名" /> <el-table-column prop="username" label="用户名" width="140" />
<el-table-column prop="position" label="岗位" /> <el-table-column prop="display_name" label="显示名称" width="140" />
<el-table-column prop="email" label="邮箱" /> <el-table-column prop="position" label="岗位" width="140" />
<el-table-column label="操作" width="250"> <el-table-column prop="email" label="邮箱" min-width="200" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button size="small" type="primary" @click="$router.push(`/admin/monitor/${row.id}/dashboard`)">工作看板</el-button> <el-button size="small" type="primary" @click="openDashboard(row)">工作看板</el-button>
<el-button size="small" type="success" @click="$router.push(`/admin/monitor/${row.id}/analysis`)">AI分析</el-button> <el-button size="small" type="success" @click="openAnalysis(row)">AI分析</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </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> </div>
</template> </template>
<script setup lang="ts"> <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' import { monitorApi } from '@/api'
const loading = ref(false) const loading = ref(false)
const employees = ref<any[]>([]) 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 loading.value = true
try { try {
const res: any = await monitorApi.getEmployees() const res: any = await monitorApi.getEmployees()
employees.value = res || [] employees.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : [])
} finally { } finally {
loading.value = false 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> </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> <el-card>
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>通知中心</span> <span>{{ isAdminMode ? '通知管理' : '通知中心' }}</span>
<div> <div v-if="!isAdminMode">
<el-tag style="margin-right: 12px" :type="wsConnected ? 'success' : 'info'"> <el-tag style="margin-right: 12px" :type="wsConnected ? 'success' : 'info'">
{{ wsConnected ? '已连接' : '未连接' }} {{ wsConnected ? '已连接' : '未连接' }}
</el-tag> </el-tag>
@ -16,7 +16,7 @@
</template> </template>
<el-tabs v-model="activeTab"> <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 class="message-list" ref="msgListRef">
<div v-for="(msg, i) in messages" :key="i" :class="['msg-item', 'msg-' + (msg.type || 'info')]"> <div v-for="(msg, i) in messages" :key="i" :class="['msg-item', 'msg-' + (msg.type || 'info')]">
<div class="msg-header"> <div class="msg-header">
@ -29,7 +29,7 @@
</div> </div>
</el-tab-pane> </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 :model="sendForm" label-width="80px" style="max-width: 500px">
<el-form-item label="标题"> <el-form-item label="标题">
<el-input v-model="sendForm.title" placeholder="通知标题" /> <el-input v-model="sendForm.title" placeholder="通知标题" />
@ -49,7 +49,7 @@
</el-form> </el-form>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="模板管理" name="templates"> <el-tab-pane v-if="isAdminMode" label="模板管理" name="templates">
<el-table :data="templates"> <el-table :data="templates">
<el-table-column prop="name" label="名称" /> <el-table-column prop="name" label="名称" />
<el-table-column prop="code" label="编码" /> <el-table-column prop="code" label="编码" />
@ -67,13 +67,16 @@
</template> </template>
<script setup lang="ts"> <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 { ElMessage } from 'element-plus'
import { notificationApi } from '@/api' import { notificationApi } from '@/api'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
const route = useRoute()
const userStore = useUserStore() 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 wsConnected = ref(false)
const messages = ref<any[]>([]) const messages = ref<any[]>([])
const sending = ref(false) const sending = ref(false)
@ -137,7 +140,7 @@ async function deleteTemplate(row: any) {
} }
onMounted(() => { onMounted(() => {
connectWs() if (!isAdminMode.value) connectWs()
loadTemplates() loadTemplates()
}) })
onUnmounted(() => disconnectWs()) 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> <el-card>
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>任务列表</span> <span>{{ pageTitle }}</span>
<el-button type="primary" @click="$router.push('/admin/task/create')" v-if="userStore.hasPermission('task:create')">创建任务</el-button>
</div> </div>
</template> </template>
@ -23,12 +22,17 @@
<el-table-column prop="deadline" label="截止日期" width="180"> <el-table-column prop="deadline" label="截止日期" width="180">
<template #default="{ row }">{{ row.deadline ? new Date(row.deadline).toLocaleDateString() : '-' }}</template> <template #default="{ row }">{{ row.deadline ? new Date(row.deadline).toLocaleDateString() : '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="260"> <el-table-column label="操作" width="160">
<template #default="{ row }"> <template #default="{ row }">
<el-button size="small" @click="$router.push(`/user/task/${row.id}`)">详情</el-button> <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
<el-button size="small" type="warning" @click="handlePush(row)">推送企微</el-button> v-if="mode === 'published' && userStore.hasPermission('task:create')"
<el-button size="small" type="danger" @click="handleDelete(row)" v-if="userStore.hasPermission('task:create')">删除</el-button> size="small"
type="danger"
@click="handleDelete(row)"
>
删除
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -37,25 +41,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { taskApi } from '@/api' import { taskApi } from '@/api'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
const router = useRouter() const route = useRoute()
const userStore = useUserStore() const userStore = useUserStore()
const loading = ref(false) const loading = ref(false)
const tasks = ref<any[]>([]) const tasks = ref<any[]>([])
async function handleDelete(row: any) { const mode = computed(() => route.path.includes('/received') ? 'received' : 'published')
try { const pageTitle = computed(() => mode.value === 'published' ? '发布的任务' : '收到的任务')
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 statusLabel = (s: string) => ({ pending: '待处理', in_progress: '进行中', completed: '已完成', cancelled: '已取消' } as any)[s] || s 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' 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] || '' const priorityType = (p: string) => ({ low: 'info', normal: '', high: 'warning', urgent: 'danger' } as any)[p] || ''
onMounted(async () => { onMounted(async () => {
await loadTasks()
})
async function loadTasks() {
loading.value = true loading.value = true
try { try {
const res: any = await taskApi.getTasks() const res: any = await taskApi.getTasks(mode.value)
tasks.value = res || [] tasks.value = res || []
} finally { } finally {
loading.value = false loading.value = false
} }
})
function editTask(row: any) {
router.push(`/admin/task/create?edit=${row.id}`)
} }
async function handlePush(row: any) { async function handleDelete(row: any) {
await taskApi.pushTask(row.id) try {
ElMessage.success('已推送') 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> </script>

Loading…
Cancel
Save