15 changed files with 614 additions and 100 deletions
@ -0,0 +1,455 @@ |
|||||
|
# Enterprise AI Platform - 问题修复与优化计划 v3 |
||||
|
|
||||
|
> 基于 log/3.log 接口报错分析 + 流编排页面问题排查 + PLAN2.md 剩余优化项 |
||||
|
> 日期: 2026-05-12 |
||||
|
> 状态: **待实施** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 一、log/3.log 接口报错分析 |
||||
|
|
||||
|
### 1.1 错误汇总 |
||||
|
|
||||
|
| # | 请求 | 状态码 | 错误类型 | 原因 | |
||||
|
|---|------|--------|---------|------| |
||||
|
| 1 | `GET /api/flow/definitions/%3Aid` | **422** | URL编码错误 | `:id` 被URL编码为 `%3Aid`,未替换为实际ID,与问题3同源 | |
||||
|
| 2 | `GET /api/wecom/config` | **500** | `NameError: name 'settings' is not defined` | `backend/modules/wecom/router.py` 第162行缺少 `from config import settings` | |
||||
|
| 3 | `ERROR modules/rag/knowledge.py:112` | (内部) | `Qdrant client is not installed` | 生产环境缺少 qdrant-client 依赖,知识检索功能不可用(非阻塞) | |
||||
|
|
||||
|
### 1.2 错误2详细分析(企微配置500) |
||||
|
|
||||
|
**文件**: `backend/modules/wecom/router.py` |
||||
|
- 第1-8行导入列表: `import uuid`, `import httpx`, `from fastapi import ...`, `from database import get_db`, `from models import User, ChatSession, ChatMessage` |
||||
|
- **缺失**: `from config import settings` ← 未导入 |
||||
|
- 第162行: `"status": "active" if settings.WECOM_CORP_ID else "unconfigured"` → `settings` 未定义 |
||||
|
- 第164行: `getattr(settings, 'WECOM_AGENT_ID', 0)` → 同样报错 |
||||
|
- 第201-202行 `update_wecom_config` 中: `hasattr(settings, key)` / `setattr(settings, key, value)` → 也用到 `settings` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 二、四个问题的根因分析与解决方案 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 问题1: `Unchecked runtime.lastError: The message port closed before a response was received.` |
||||
|
|
||||
|
#### 根因分析 |
||||
|
|
||||
|
这不是应用代码的bug,而是 **Chrome浏览器扩展(Extension)** 产生的警告。当浏览器扩展(如 Vue DevTools、广告拦截器、翻译插件等)尝试通过消息端口与页面通信时,如果端口在收到响应前被关闭,就会触发此警告。 |
||||
|
|
||||
|
具体触发路径: |
||||
|
1. 页面URL: `/#/admin/flow/editor` |
||||
|
2. 页面包含 iframe 或使用 `chrome.runtime` API 的扩展 |
||||
|
3. 扩展的消息端口在接收方响应前被关闭 |
||||
|
|
||||
|
#### 解决方案 |
||||
|
|
||||
|
**方案A(推荐)**: 在前端全局添加运行时错误过滤 |
||||
|
|
||||
|
文件: `frontend/src/main.ts` |
||||
|
|
||||
|
```typescript |
||||
|
// 过滤 Chrome 扩展产生的非关键错误 |
||||
|
if (typeof chrome !== 'undefined' && chrome.runtime) { |
||||
|
const originalError = window.onerror |
||||
|
window.onerror = function(message, source, lineno, colno, error) { |
||||
|
if (typeof message === 'string' && message.includes('message port closed')) { |
||||
|
return true // 静默忽略扩展通信错误 |
||||
|
} |
||||
|
if (originalError) { |
||||
|
return originalError(message, source, lineno, colno, error) |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**方案B**: 在 `index.html` 中添加: |
||||
|
```html |
||||
|
<script> |
||||
|
window.addEventListener('error', function(e) { |
||||
|
if (e.message && e.message.includes('message port closed')) { |
||||
|
e.stopPropagation() |
||||
|
e.preventDefault() |
||||
|
} |
||||
|
}, true) |
||||
|
</script> |
||||
|
``` |
||||
|
|
||||
|
**影响范围**: 全局,不影响任何业务功能 |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 问题2: 节点面板的节点无法拖到画布上(无任何报错) |
||||
|
|
||||
|
#### 根因分析 |
||||
|
|
||||
|
**文件**: `frontend/src/views/flow/FlowEditor.vue` |
||||
|
|
||||
|
问题出在**拖拽事件使用了错误的DOM事件类型**,导致 `dataTransfer` 始终为 `null`: |
||||
|
|
||||
|
| 位置 | 当前代码 | 问题 | |
||||
|
|------|---------|------| |
||||
|
| 第25行 | `@mousedown="onDragStart($event, node)"` | ❌ 使用了 `mousedown` 事件,该事件对象上 **没有** `dataTransfer` 属性 | |
||||
|
| 第253行 | `function onDragStart(event: MouseEvent, ...)` | ❌ `MouseEvent` 类型上不存在 `dataTransfer` | |
||||
|
| 第255行 | `const dt = (event as any).dataTransfer` | ❌ 强行转类型,`mousedown` 事件的 `dataTransfer` 永远是 `undefined`,导致 `if (dt)` 条件永不成立,节点数据从未被设置 | |
||||
|
| 第29行 | `<div class="node-item">` | ❌ 缺少 `draggable="true"` 属性,即使改用 `dragstart` 事件也无法触发拖拽 | |
||||
|
|
||||
|
**核心问题链**: |
||||
|
1. `mousedown` 事件没有 `dataTransfer` → `dt` 为 `undefined` |
||||
|
2. `if (vueFlowRef.value && dt)` 条件永远为 `false` |
||||
|
3. `dt.setData('application/vueflow', ...)` 永远不执行 |
||||
|
4. `onDrop` 中 `event.dataTransfer?.getData('application/vueflow')` 返回 `null` |
||||
|
5. 函数直接 `return`,不创建任何节点 |
||||
|
|
||||
|
**为什么"没有任何报错"**: 因为代码流程中每个 `if` 条件都静默返回了 `null/undefined`,没有走到会抛异常的逻辑路径。 |
||||
|
|
||||
|
#### 解决方案 |
||||
|
|
||||
|
**修改文件**: `frontend/src/views/flow/FlowEditor.vue` |
||||
|
|
||||
|
**修改点1** — 模板: 将 `@mousedown` 改为 `@dragstart`,添加 `draggable="true"` |
||||
|
|
||||
|
第21-29行,改为: |
||||
|
```html |
||||
|
<div |
||||
|
v-for="node in nodeTypes" |
||||
|
:key="node.type" |
||||
|
class="node-item" |
||||
|
draggable="true" |
||||
|
@dragstart="onDragStart($event, node)" |
||||
|
> |
||||
|
``` |
||||
|
|
||||
|
**修改点2** — 脚本: 修复 `onDragStart` 函数 |
||||
|
|
||||
|
第253-259行,改为: |
||||
|
```typescript |
||||
|
function onDragStart(event: DragEvent, node: (typeof nodeTypes)[0]) { |
||||
|
if (event.dataTransfer) { |
||||
|
event.dataTransfer.setData('application/vueflow', JSON.stringify(node)) |
||||
|
event.dataTransfer.effectAllowed = 'move' |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**修改点3** — 脚本: 增强 `onDrop` 函数的健壮性 |
||||
|
|
||||
|
第261-291行,改为: |
||||
|
```typescript |
||||
|
function onDrop(event: DragEvent) { |
||||
|
const dataStr = event.dataTransfer?.getData('application/vueflow') |
||||
|
if (!dataStr) return |
||||
|
|
||||
|
const nodeData = JSON.parse(dataStr) |
||||
|
|
||||
|
// 使用 Vue Flow 内置方法计算画布坐标,回退到手动估算 |
||||
|
let position = { x: 100, y: 100 } |
||||
|
try { |
||||
|
const bounds = (event.currentTarget as HTMLElement)?.getBoundingClientRect() |
||||
|
if (bounds) { |
||||
|
position = { |
||||
|
x: event.clientX - bounds.left - 80, |
||||
|
y: event.clientY - bounds.top - 20, |
||||
|
} |
||||
|
} |
||||
|
} catch { /* fallback to default position */ } |
||||
|
|
||||
|
const id = `node_${nodeCounter++}` |
||||
|
const newNode: any = { |
||||
|
id, |
||||
|
type: 'custom', |
||||
|
position, |
||||
|
data: { |
||||
|
label: nodeData.label, |
||||
|
type: nodeData.type, |
||||
|
typeDesc: nodeData.typeDesc, |
||||
|
color: colorMap[nodeData.type] || '#409EFF', |
||||
|
config: nodeData.type === 'llm' |
||||
|
? { system_prompt: '', model: 'gpt-4o-mini', temperature: 0.7 } |
||||
|
: {}, |
||||
|
}, |
||||
|
draggable: true, |
||||
|
connectable: true, |
||||
|
} |
||||
|
|
||||
|
elements.value = [...elements.value, newNode] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 问题3: 流详情 `/api/flow/definitions/:id` 直接在菜单中出现,未传ID导致422错误 |
||||
|
|
||||
|
#### 根因分析 |
||||
|
|
||||
|
**问题源**: `frontend/src/components/layout/AdminLayout.vue` 第46行: |
||||
|
|
||||
|
```html |
||||
|
<el-menu-item index="/admin/flow/market/:id">流详情</el-menu-item> |
||||
|
``` |
||||
|
|
||||
|
这段代码存在两个严重问题: |
||||
|
|
||||
|
1. **`:id` 是路由参数占位符,不能直接作为菜单导航路径**: |
||||
|
- 点击菜单 → `router.push('/admin/flow/market/:id')` |
||||
|
- 前端路由匹配路径 `/admin/flow/market/:id` → `route.params.id` = `':id'` (字面量) |
||||
|
- 组件 `FlowDetail.vue` 调用 API `flowApi.getFlow(':id')` |
||||
|
- 发送请求 `GET /api/flow/definitions/%3Aid` → 422 Unprocessable Entity |
||||
|
|
||||
|
2. **业务逻辑错误**: 流详情是**列表项的操作入口**(点击某条流记录 → 查看详情),而不是一个独立的导航菜单项。用户必须先选择一个具体的流,才能查看其详情。 |
||||
|
|
||||
|
**对比已有的正确设计**: |
||||
|
- `TaskDetail` 路由: `/user/task/:id` — **不在菜单中**,只能通过任务列表点击跳转 |
||||
|
- `FlowEditor` 编辑路由: `/admin/flow/editor/:id` — **不在菜单中**,通过流列表点击跳转 |
||||
|
- `FlowDetail` 流详情路由: `/admin/flow/market/:id` — **错误地放在菜单中** |
||||
|
|
||||
|
#### 解决方案 |
||||
|
|
||||
|
**修改文件1**: `frontend/src/components/layout/AdminLayout.vue` |
||||
|
|
||||
|
删除第46行: |
||||
|
```diff |
||||
|
- <el-menu-item index="/admin/flow/market/:id">流详情</el-menu-item> |
||||
|
``` |
||||
|
|
||||
|
同时检查 `FlowList.vue` 和 `FlowMarket.vue` 是否已有正确的详情跳转入口(Card点击 → `router.push('/admin/flow/market/:id')`),如果没有则需要补充。 |
||||
|
|
||||
|
**修改文件2**: `frontend/src/views/flow/FlowList.vue` — 确保每条流记录点击能跳转到详情 |
||||
|
|
||||
|
检查并确保 FlowList 每行有跳转逻辑: |
||||
|
```typescript |
||||
|
function viewFlow(id: string) { |
||||
|
router.push(`/admin/flow/market/${id}`) |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### 问题4: 管理后台和企业AI后台 —— 做成固定在右上角账号隔壁的选择项 |
||||
|
|
||||
|
#### 根因分析 |
||||
|
|
||||
|
**当前实现位置**: |
||||
|
- `MainLayout.vue` 第72-75行: "管理后台" 入口放在 **左侧边栏底部**(仅管理员可见) |
||||
|
- `AdminLayout.vue` 第86-89行: "返回用户端" 入口放在 **左侧边栏底部** |
||||
|
|
||||
|
**存在的问题**: |
||||
|
1. 入口位置隐蔽,用户不易发现 |
||||
|
2. "管理后台"入口被混在功能菜单中,不够突出 |
||||
|
3. 两个入口位于不同布局,缺少统一的切换组件 |
||||
|
4. 没有明确的视觉状态指示当前所在端 |
||||
|
|
||||
|
**需求**: 做成固定在右上角、账号头像旁边的**明确可切换的选择项** |
||||
|
|
||||
|
#### 解决方案 |
||||
|
|
||||
|
**方案**: 在两个 Layout 的 `header-right` 区域统一添加"端切换"组件 |
||||
|
|
||||
|
##### 4.1 创建公共切换组件 |
||||
|
|
||||
|
文件: `frontend/src/components/common/PortalSwitcher.vue` |
||||
|
|
||||
|
```vue |
||||
|
<template> |
||||
|
<div class="portal-switcher"> |
||||
|
<el-radio-group |
||||
|
v-model="currentPortal" |
||||
|
size="small" |
||||
|
@change="handleSwitch" |
||||
|
fill="#409EFF" |
||||
|
> |
||||
|
<el-radio-button value="user"> |
||||
|
<el-icon><Monitor /></el-icon> |
||||
|
<span class="portal-label">企业AI</span> |
||||
|
</el-radio-button> |
||||
|
<el-radio-button value="admin" v-if="canAccessAdmin"> |
||||
|
<el-icon><Setting /></el-icon> |
||||
|
<span class="portal-label">管理后台</span> |
||||
|
</el-radio-button> |
||||
|
</el-radio-group> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { computed } from 'vue' |
||||
|
import { useRoute, useRouter } from 'vue-router' |
||||
|
import { useUserStore } from '@/stores/user' |
||||
|
import { Monitor, Setting } from '@element-plus/icons-vue' |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const router = useRouter() |
||||
|
const userStore = useUserStore() |
||||
|
|
||||
|
const canAccessAdmin = computed(() => userStore.isAdmin) |
||||
|
|
||||
|
const currentPortal = computed(() => { |
||||
|
return route.path.startsWith('/admin') ? 'admin' : 'user' |
||||
|
}) |
||||
|
|
||||
|
function handleSwitch(portal: string) { |
||||
|
if (portal === 'user' && route.path.startsWith('/admin')) { |
||||
|
router.push('/user/dashboard') |
||||
|
} else if (portal === 'admin' && !route.path.startsWith('/admin')) { |
||||
|
router.push('/admin') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.portal-switcher { |
||||
|
margin-right: 16px; |
||||
|
} |
||||
|
.portal-label { |
||||
|
margin-left: 4px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
:deep(.el-radio-button__inner) { |
||||
|
padding: 5px 12px; |
||||
|
} |
||||
|
</style> |
||||
|
``` |
||||
|
|
||||
|
##### 4.2 修改 MainLayout.vue |
||||
|
|
||||
|
第88-102行区域的 `header-right`,在用户下拉菜单前插入 PortalSwitcher: |
||||
|
|
||||
|
```diff |
||||
|
<div class="header-right"> |
||||
|
+ <PortalSwitcher /> |
||||
|
<el-dropdown @command="handleCommand"> |
||||
|
... |
||||
|
</el-dropdown> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
同时**移除**第72-75行的侧边栏管理后台入口: |
||||
|
```diff |
||||
|
- <el-menu-item v-if="userStore.isAdmin" index="/admin" style="margin-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px"> |
||||
|
- <el-icon><Setting /></el-icon> |
||||
|
- <span>管理后台</span> |
||||
|
- </el-menu-item> |
||||
|
``` |
||||
|
|
||||
|
##### 4.3 修改 AdminLayout.vue |
||||
|
|
||||
|
同样在 header-right 区域插入 PortalSwitcher: |
||||
|
|
||||
|
```diff |
||||
|
<div class="header-right"> |
||||
|
+ <PortalSwitcher /> |
||||
|
<el-dropdown @command="handleCommand"> |
||||
|
... |
||||
|
</el-dropdown> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
同时**移除**第86-89行的侧边栏返回用户端入口: |
||||
|
```diff |
||||
|
- <el-menu-item index="/user/dashboard" style="margin-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 10px"> |
||||
|
- <el-icon><User /></el-icon> |
||||
|
- <span>返回用户端</span> |
||||
|
- </el-menu-item> |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 三、P3 级体验优化(下期规划) |
||||
|
|
||||
|
> 来源: PLAN2.md "六、剩余可优化项" |
||||
|
|
||||
|
### 3.1 全局搜索 |
||||
|
|
||||
|
| 项目 | 详情 | |
||||
|
|------|------| |
||||
|
| **入口位置** | 顶部导航栏中央,全局搜索框(Cmd/Ctrl + K 快捷键唤起) | |
||||
|
| **搜索范围** | 任务名称/ID、流名称/ID、员工姓名、部门名称、智能体类型 | |
||||
|
| **交互设计** | 输入关键词 → 实时下拉联想 → 选中 → 跳转到对应详情页 | |
||||
|
| **技术方案** | 前端: `el-autocomplete` + 防抖;后端: 新增 `GET /api/search?q=keyword` 聚合搜索接口 | |
||||
|
| **前端文件** | 新增 `components/common/GlobalSearch.vue` | |
||||
|
| **后端文件** | 新增 `modules/search/router.py` | |
||||
|
|
||||
|
### 3.2 主题切换(深色/浅色模式) |
||||
|
|
||||
|
| 项目 | 详情 | |
||||
|
|------|------| |
||||
|
| **入口位置** | 右上角 PortalSwitcher 旁边,添加主题切换按钮(太阳/月亮图标) | |
||||
|
| **切换内容** | Element Plus CSS 变量覆盖 + 自定义页面背景色 | |
||||
|
| **持久化** | localStorage 存 `theme` 字段,刷新后保持 | |
||||
|
| **技术方案** | 使用 `el-config-provider` 的 `namespace` + CSS 变量覆盖。Element Plus 2.x 原生支持暗黑模式(添加 `dark` class 到 `html` 元素) | |
||||
|
| **前端文件** | 新增 `stores/theme.ts` + 修改 `main.ts` | |
||||
|
| **CSS文件** | 新增 `styles/dark.css` | |
||||
|
|
||||
|
### 3.3 国际化(i18n 多语言) |
||||
|
|
||||
|
| 项目 | 详情 | |
||||
|
|------|------| |
||||
|
| **支持语言** | 简体中文(zh-CN)、英文(en-US) | |
||||
|
| **实现方案** | vue-i18n ^9.x,语言包按模块拆分 | |
||||
|
| **入口位置** | 右上角 PortalSwitcher 旁边,语言切换下拉或图标 | |
||||
|
| **持久化** | localStorage 存 `locale` 字段 | |
||||
|
| **翻译范围** | 菜单、按钮、表单标签、提示信息、表格列头、错误信息 | |
||||
|
| **前端文件** | 新增 `locales/zh-CN.ts`、`locales/en-US.ts`、`locales/index.ts` | |
||||
|
|
||||
|
### 3.4 新手引导(首次登录操作指引) |
||||
|
|
||||
|
| 项目 | 详情 | |
||||
|
|------|------| |
||||
|
| **触发条件** | 用户表中 `onboarding_completed` 字段为 false | |
||||
|
| **引导步骤** | ① 欢迎 → ② 查看工作台 → ③ 创建任务 → ④ 查看智能体 → ⑤ 进入管理后台(仅管理员) | |
||||
|
| **实现方案** | driver.js 或 intro.js 库,高亮目标元素 + 文字提示泡泡 | |
||||
|
| **跳过机制** | 引导面板有"跳过"和"不再显示"按钮 | |
||||
|
| **前端文件** | 新增 `components/common/OnboardingGuide.vue` | |
||||
|
| **后端文件** | `PUT /api/auth/me` 增加 `onboarding_completed` 字段支持 | |
||||
|
|
||||
|
### 3.5 移动端适配(响应式布局) |
||||
|
|
||||
|
| 项目 | 详情 | |
||||
|
|------|------| |
||||
|
| **适配范围** | 工作台、任务列表、智能体对话、通知中心(核心用户端功能) | |
||||
|
| **布局策略** | 768px 以下: 侧边栏隐藏(汉堡菜单),内容区全宽,表格横向滚动 | |
||||
|
| **组件调整** | el-card → 边距缩小,el-table → 固定列+横向滚动,el-form → 标签在上 | |
||||
|
| **CSS方案** | 全局 `@media (max-width: 768px)` + Element Plus 响应式断点 | |
||||
|
| **管理后台** | 仅做基本适配(可左右滚动),不做深度移动端优化 | |
||||
|
| **前端文件** | 新增 `styles/mobile.css` + 各页面添加响应式 scoped style | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 四、实施优先级与排期 |
||||
|
|
||||
|
### 4.1 本期(P0 紧急修复) |
||||
|
|
||||
|
| # | 任务 | 文件 | 工作量 | |
||||
|
|---|------|------|--------| |
||||
|
| 1 | 修复企微配置500错误(缺少settings导入) | `backend/modules/wecom/router.py` | 5分钟 | |
||||
|
| 2 | 修复节点拖拽(mousedown→dragstart + draggable) | `frontend/src/views/flow/FlowEditor.vue` | 15分钟 | |
||||
|
| 3 | 移除菜单中的"流详情"(`:id`占位符) | `frontend/src/components/layout/AdminLayout.vue` | 5分钟 | |
||||
|
| 4 | PortalSwitcher 创建并集成到两个Layout | `frontend/src/components/common/PortalSwitcher.vue` + `MainLayout.vue` + `AdminLayout.vue` | 30分钟 | |
||||
|
| 5 | 过滤 Chrome 扩展 runtime.lastError | `frontend/src/main.ts` 或 `index.html` | 5分钟 | |
||||
|
|
||||
|
### 4.2 下期(P3 体验优化) |
||||
|
|
||||
|
| # | 任务 | 工作量 | 建议排期 | |
||||
|
|---|------|--------|---------| |
||||
|
| 6 | 全局搜索 | 2天 | Week 1 | |
||||
|
| 7 | 主题切换(深色/浅色) | 1天 | Week 1 | |
||||
|
| 8 | 国际化 i18n | 2天 | Week 2 | |
||||
|
| 9 | 新手引导 | 1天 | Week 2 | |
||||
|
| 10 | 移动端适配 | 2天 | Week 2-3 | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 五、验证检查清单 |
||||
|
|
||||
|
实施完成后,使用 **sroot** 账号验证: |
||||
|
|
||||
|
``` |
||||
|
□ 企微配置页面正常加载(不再500报错) |
||||
|
□ 节点面板拖拽到画布: 拖拽正常 → 节点出现在画布上 |
||||
|
□ 菜单中不再出现"流详情"菜单项 |
||||
|
□ 从流列表/流市场点击具体流 → 正确跳转流详情 |
||||
|
□ 右上角显示 PortalSwitcher 切换按钮 |
||||
|
□ 在企业AI端 → 点击"管理后台" → 切换到管理后台 |
||||
|
□ 在管理后台 → 点击"企业AI" → 切换回企业AI端 |
||||
|
□ 流编辑器页面加载无 Chrome runtime.lastError 警告 |
||||
|
``` |
||||
@ -0,0 +1,56 @@ |
|||||
|
<template> |
||||
|
<div class="portal-switcher"> |
||||
|
<el-radio-group |
||||
|
:model-value="currentPortal" |
||||
|
size="small" |
||||
|
@change="handleSwitch" |
||||
|
> |
||||
|
<el-radio-button value="user"> |
||||
|
<el-icon><Monitor /></el-icon> |
||||
|
<span class="portal-label">企业AI</span> |
||||
|
</el-radio-button> |
||||
|
<el-radio-button value="admin" v-if="canAccessAdmin"> |
||||
|
<el-icon><Setting /></el-icon> |
||||
|
<span class="portal-label">管理后台</span> |
||||
|
</el-radio-button> |
||||
|
</el-radio-group> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script setup lang="ts"> |
||||
|
import { computed } from 'vue' |
||||
|
import { useRoute, useRouter } from 'vue-router' |
||||
|
import { useUserStore } from '@/stores/user' |
||||
|
import { Monitor, Setting } from '@element-plus/icons-vue' |
||||
|
|
||||
|
const route = useRoute() |
||||
|
const router = useRouter() |
||||
|
const userStore = useUserStore() |
||||
|
|
||||
|
const canAccessAdmin = computed(() => userStore.isAdmin) |
||||
|
|
||||
|
const currentPortal = computed(() => { |
||||
|
return route.path.startsWith('/admin') ? 'admin' : 'user' |
||||
|
}) |
||||
|
|
||||
|
function handleSwitch(portal: string) { |
||||
|
if (portal === 'user' && route.path.startsWith('/admin')) { |
||||
|
router.push('/user/dashboard') |
||||
|
} else if (portal === 'admin' && !route.path.startsWith('/admin')) { |
||||
|
router.push('/admin') |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.portal-switcher { |
||||
|
margin-right: 16px; |
||||
|
} |
||||
|
.portal-label { |
||||
|
margin-left: 4px; |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
:deep(.el-radio-button__inner) { |
||||
|
padding: 5px 12px; |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue