本地 RAG 知识库
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

/**
* 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')