You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
14 KiB
342 lines
14 KiB
/**
|
|
* Vue 应用入口
|
|
* 侧边栏导航 + 内容区布局,数据驱动菜单渲染
|
|
* P1: 登录守卫 + 用户信息展示 + 新页面注册
|
|
*/
|
|
import { createApp } from 'vue'
|
|
import { store, MENU_ITEMS } from './store.js'
|
|
import { isLoggedIn, clearTokens, getUserInfo, toast, getRefreshToken } from './utils.js'
|
|
import { getCurrentUser, refreshToken } from './api.js'
|
|
|
|
// 导入组件
|
|
import ChatPanel from '../components/ChatPanel.js'
|
|
import DocStats from '../components/DocStats.js'
|
|
import DocSearch from '../components/DocSearch.js'
|
|
import CategoryManager from '../components/CategoryManager.js'
|
|
import RoleManager from '../components/RoleManager.js'
|
|
import DocList from '../components/DocList.js'
|
|
import DocUpload from '../components/DocUpload.js'
|
|
import DocDetail from '../components/DocDetail.js'
|
|
import ConversationManager from '../components/ConversationManager.js'
|
|
import ModelConfigManager from '../components/ModelConfigManager.js'
|
|
import SensitiveWordManager from '../components/SensitiveWordManager.js'
|
|
import FaqManager from '../components/FaqManager.js'
|
|
import AuditLogManager from '../components/AuditLogManager.js'
|
|
import LoginPage from '../components/LoginPage.js'
|
|
import UserManager from '../components/UserManager.js'
|
|
import DashboardPanel from '../components/DashboardPanel.js'
|
|
import ApiKeyManager from '../components/ApiKeyManager.js'
|
|
import WebhookManager from '../components/WebhookManager.js'
|
|
import McpServerManager from '../components/McpServerManager.js'
|
|
|
|
const app = createApp({
|
|
setup() {
|
|
const menuItems = MENU_ITEMS
|
|
|
|
/** 判断父菜单是否高亮(任一子页面激活时高亮父菜单) */
|
|
function isMenuActive(parentItem) {
|
|
if (!parentItem.children) return store.activePage === parentItem.id
|
|
return parentItem.children.some(c => c.id === store.activePage)
|
|
}
|
|
|
|
/** 判断菜单项是否对当前用户可见(roles 限制) */
|
|
function isMenuVisible(item) {
|
|
if (!item.roles || !item.roles.length) return true
|
|
return item.roles.some(r => store.hasRole(r))
|
|
}
|
|
|
|
/** 登录成功回调 */
|
|
function onLoginSuccess(user) {
|
|
store.currentUser = user || getUserInfo()
|
|
store.isLoggedIn = true
|
|
}
|
|
|
|
/** 退出登录 */
|
|
function handleLogout() {
|
|
clearTokens()
|
|
store.isLoggedIn = false
|
|
store.currentUser = {}
|
|
store.activePage = 'chat'
|
|
toast('已退出登录', 'info')
|
|
}
|
|
|
|
/** 尝试刷新 Token */
|
|
async function tryRefreshToken() {
|
|
const rt = getRefreshToken()
|
|
if (!rt) return false
|
|
try {
|
|
const res = await refreshToken(rt)
|
|
if (res.success && res.data) {
|
|
const { setToken: st, setRefreshToken: srt } = await import('./utils.js')
|
|
st(res.data.accessToken)
|
|
srt(res.data.refreshToken)
|
|
return true
|
|
}
|
|
} catch (e) { /* 刷新失败 */ }
|
|
return false
|
|
}
|
|
|
|
// 监听 401 未授权事件
|
|
window.addEventListener('auth:unauthorized', async () => {
|
|
const refreshed = await tryRefreshToken()
|
|
if (!refreshed) {
|
|
store.isLoggedIn = false
|
|
store.currentUser = {}
|
|
toast('登录已过期,请重新登录', 'error')
|
|
}
|
|
})
|
|
|
|
// 启动时验证登录态
|
|
if (isLoggedIn()) {
|
|
getCurrentUser().then(res => {
|
|
if (res.success && res.data) {
|
|
store.currentUser = res.data
|
|
store.isLoggedIn = true
|
|
} else {
|
|
// 业务失败(如用户被禁用),尝试刷新
|
|
tryRefreshToken().then(ok => {
|
|
if (!ok) {
|
|
store.isLoggedIn = false
|
|
clearTokens()
|
|
} else {
|
|
// 刷新成功后重新获取用户信息
|
|
getCurrentUser().then(r => {
|
|
if (r.success && r.data) {
|
|
store.currentUser = r.data
|
|
store.isLoggedIn = true
|
|
} else {
|
|
store.isLoggedIn = false
|
|
clearTokens()
|
|
}
|
|
}).catch(() => { store.isLoggedIn = false; clearTokens() })
|
|
}
|
|
})
|
|
}
|
|
}).catch(() => {
|
|
// 401 等错误,尝试刷新(不要先清除 token!)
|
|
tryRefreshToken().then(ok => {
|
|
if (!ok) {
|
|
store.isLoggedIn = false
|
|
clearTokens()
|
|
} else {
|
|
// 刷新成功后重新获取用户信息
|
|
getCurrentUser().then(r => {
|
|
if (r.success && r.data) {
|
|
store.currentUser = r.data
|
|
store.isLoggedIn = true
|
|
} else {
|
|
store.isLoggedIn = false
|
|
clearTokens()
|
|
}
|
|
}).catch(() => { store.isLoggedIn = false; clearTokens() })
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
return { menuItems, isMenuActive, isMenuVisible, store, onLoginSuccess, handleLogout }
|
|
},
|
|
template: `
|
|
<!-- 未登录:显示登录页 -->
|
|
<login-page v-if="!store.isLoggedIn" @login-success="onLoginSuccess"></login-page>
|
|
|
|
<!-- 已登录:显示管理后台 -->
|
|
<template v-else>
|
|
<!-- 移动端遮罩层 -->
|
|
<div class="sidebar-overlay" v-if="store.mobileMenuOpen" @click="store.mobileMenuOpen = false"></div>
|
|
|
|
<!-- 顶部导航 -->
|
|
<div class="topbar">
|
|
<button class="menu-toggle" @click="store.mobileMenuOpen = !store.mobileMenuOpen">☰</button>
|
|
<span class="logo">🤖 Support Bot</span><span class="ver">AI 智能客服系统</span>
|
|
<span class="links">
|
|
<a href="/sdk/test.html" target="_blank">🧪 SDK 测试面板</a>
|
|
<a href="/doc.html" target="_blank">📖 API 文档</a>
|
|
</span>
|
|
<!-- 用户信息 + 退出 -->
|
|
<div class="user-bar">
|
|
<span class="user-info">
|
|
<span class="user-avatar">{{ (store.currentUser.nickname || store.currentUser.username || 'U').charAt(0) }}</span>
|
|
<span class="user-name">{{ store.currentUser.nickname || store.currentUser.username || '' }}</span>
|
|
<span class="user-role-tag" v-for="r in (store.currentUser.roles || [])" :key="r">{{ r }}</span>
|
|
</span>
|
|
<button class="btn btn-sm btn-logout" @click="handleLogout" title="退出登录">退出</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 布局主体:侧边栏 + 内容区 -->
|
|
<div class="layout-body">
|
|
|
|
<!-- 左侧边栏 -->
|
|
<aside class="sidebar" :class="{ collapsed: store.sidebarCollapsed && !store.mobileMenuOpen, open: store.mobileMenuOpen }">
|
|
<div class="sidebar-inner">
|
|
<nav class="sidebar-nav">
|
|
<template v-for="item in menuItems" :key="item.id">
|
|
|
|
<!-- 有子菜单的父级菜单项 -->
|
|
<div v-if="item.children" :class="['menu-group', store.expandedMenus.includes(item.id) ? 'open' : '']">
|
|
<button
|
|
class="menu-item menu-parent"
|
|
:class="{ active: isMenuActive(item) }"
|
|
@click="store.toggleMenu(item.id)"
|
|
:title="store.sidebarCollapsed ? item.label : ''"
|
|
>
|
|
<span class="menu-icon">{{ item.icon }}</span>
|
|
<span class="menu-label">{{ item.label }}</span>
|
|
<span class="menu-arrow" :class="{ expanded: store.expandedMenus.includes(item.id) }">▾</span>
|
|
</button>
|
|
<!-- 子菜单项(按角色过滤) -->
|
|
<div class="submenu" v-show="store.expandedMenus.includes(item.id) && !store.sidebarCollapsed">
|
|
<button
|
|
v-for="child in item.children.filter(c => isMenuVisible(c))"
|
|
:key="child.id"
|
|
class="menu-item menu-child"
|
|
:class="{ active: store.activePage === child.id }"
|
|
@click="store.navigateTo(child.id)"
|
|
>
|
|
<span class="menu-icon child-icon">{{ child.icon }}</span>
|
|
<span class="menu-label">{{ child.label }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 无子菜单的直接导航项 -->
|
|
<button
|
|
v-else
|
|
class="menu-item"
|
|
:class="{ active: store.activePage === item.id }"
|
|
@click="store.navigateTo(item.id)"
|
|
:title="store.sidebarCollapsed ? item.label : ''"
|
|
>
|
|
<span class="menu-icon">{{ item.icon }}</span>
|
|
<span class="menu-label">{{ item.label }}</span>
|
|
</button>
|
|
|
|
</template>
|
|
</nav>
|
|
|
|
<!-- 侧边栏折叠按钮 -->
|
|
<button class="sidebar-collapse-btn" @click="store.toggleSidebar()">
|
|
<span :style="{ transform: store.sidebarCollapsed ? 'rotate(180deg)' : '' }">◀</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- 右侧内容区 -->
|
|
<main class="main" :class="{ 'main-chat': store.activePage === 'chat' }">
|
|
|
|
<!-- 智能客服对话 -->
|
|
<div v-if="store.activePage === 'chat'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<chat-panel></chat-panel>
|
|
</div>
|
|
|
|
<!-- 知识库 - 统计概览 -->
|
|
<div v-if="store.activePage === 'stats'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<doc-stats></doc-stats>
|
|
</div>
|
|
|
|
<!-- 知识库 - 文档管理 -->
|
|
<div v-if="store.activePage === 'doc-manage'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<doc-list></doc-list>
|
|
<doc-upload></doc-upload>
|
|
</div>
|
|
|
|
<!-- 知识库 - 分类管理 -->
|
|
<div v-if="store.activePage === 'category'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<category-manager></category-manager>
|
|
</div>
|
|
|
|
<!-- 知识库 - 搜索测试 -->
|
|
<div v-if="store.activePage === 'search-test'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<doc-search></doc-search>
|
|
</div>
|
|
|
|
<!-- 知识库 - FAQ 管理 -->
|
|
<div v-if="store.activePage === 'faq'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<faq-manager></faq-manager>
|
|
</div>
|
|
|
|
<!-- 会话管理 -->
|
|
<div v-if="store.activePage === 'conversation'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<conversation-manager></conversation-manager>
|
|
</div>
|
|
|
|
<!-- 运营看板 -->
|
|
<div v-if="store.activePage === 'dashboard'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<dashboard-panel></dashboard-panel>
|
|
</div>
|
|
|
|
|
|
<!-- 系统设置 - 客服角色管理 -->
|
|
<div v-if="store.activePage === 'role'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<role-manager></role-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - 模型配置 -->
|
|
<div v-if="store.activePage === 'model-config'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<model-config-manager></model-config-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - 敏感词管理 -->
|
|
<div v-if="store.activePage === 'sensitive'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<sensitive-word-manager></sensitive-word-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - 审计日志 -->
|
|
<div v-if="store.activePage === 'audit-log'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<audit-log-manager></audit-log-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - 用户管理 -->
|
|
<div v-if="store.activePage === 'user-manage'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<user-manager></user-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - API Key 管理 -->
|
|
<div v-if="store.activePage === 'api-key'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<api-key-manager></api-key-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - Webhook 管理 -->
|
|
<div v-if="store.activePage === 'webhook'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<webhook-manager></webhook-manager>
|
|
</div>
|
|
|
|
<!-- 系统设置 - MCP 服务管理 -->
|
|
<div v-if="store.activePage === 'mcp-server'" class="page-pane" style="animation: fadeIn .3s ease;">
|
|
<mcp-server-manager></mcp-server-manager>
|
|
</div>
|
|
|
|
<!-- 文档详情弹窗 -->
|
|
<doc-detail></doc-detail>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Toast 容器 -->
|
|
<div class="toast-container" id="toasts"></div>
|
|
`
|
|
})
|
|
|
|
// 注册组件
|
|
app.component('chat-panel', ChatPanel)
|
|
app.component('doc-stats', DocStats)
|
|
app.component('doc-search', DocSearch)
|
|
app.component('category-manager', CategoryManager)
|
|
app.component('role-manager', RoleManager)
|
|
app.component('doc-list', DocList)
|
|
app.component('doc-upload', DocUpload)
|
|
app.component('doc-detail', DocDetail)
|
|
app.component('conversation-manager', ConversationManager)
|
|
app.component('model-config-manager', ModelConfigManager)
|
|
app.component('sensitive-word-manager', SensitiveWordManager)
|
|
app.component('faq-manager', FaqManager)
|
|
app.component('audit-log-manager', AuditLogManager)
|
|
app.component('login-page', LoginPage)
|
|
app.component('user-manager', UserManager)
|
|
app.component('dashboard-panel', DashboardPanel)
|
|
app.component('api-key-manager', ApiKeyManager)
|
|
app.component('webhook-manager', WebhookManager)
|
|
app.component('mcp-server-manager', McpServerManager)
|
|
|
|
app.mount('#app')
|