-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
+
+
-
-
+
+
-
+
-
-
-
-
-
+
+
-
-
+
+
@@ -347,13 +343,6 @@ export default {
}
})
- // 监听自定义模型名输入
- watch(customModelName, (val) => {
- if (editModal.value.form.model_name === '__custom__' && val) {
- editModal.value.form.model_name = val
- }
- })
-
// ==================== 数据加载 ====================
async function load(p = 1) {
diff --git a/src/main/resources/static/components/SensitiveWordManager.js b/src/main/resources/static/components/SensitiveWordManager.js
index 7e82186..b00c4b6 100644
--- a/src/main/resources/static/components/SensitiveWordManager.js
+++ b/src/main/resources/static/components/SensitiveWordManager.js
@@ -2,7 +2,7 @@
* 敏感词管理组件
* 支持 CRUD + 批量导入 + 审计日志查看
*/
-import { listSensitiveWords, createSensitiveWord, updateSensitiveWord, deleteSensitiveWord, batchImportSensitiveWords, listAuditLogs } from '../js/api.js'
+import { listSensitiveWords, createSensitiveWord, updateSensitiveWord, deleteSensitiveWord, batchImportSensitiveWords } from '../js/api.js'
import { toast } from '../js/utils.js'
export default {
@@ -14,7 +14,6 @@ export default {
-
-
-
-
-
📋 内容审计日志
-
-
-
- | 时间 |
- 方向 |
- 违规内容 |
- 命中词 |
- 处理 |
-
-
-
- | 暂无审计日志 |
-
- | {{ formatTime(log.createTime) }} |
- {{ log.direction === 'INPUT' ? '输入' : '输出' }} |
- {{ log.originalText }} |
- {{ formatHitWords(log.hitWords) }} |
-
-
- {{ log.actionTaken === 'BLOCK' ? '拦截' : log.actionTaken === 'MASK' ? '脱敏' : '通过' }}
-
- |
-
-
-
-
`,
@@ -164,13 +133,11 @@ export default {
filterCategory: '',
showAddDialog: false,
showImportDialog: false,
- showAuditLog: false,
editingWord: null,
form: { word: '', category: 'custom', level: 1, remark: '' },
importText: '',
importCategory: 'custom',
importLevel: 1,
- auditLogs: [],
searchTimer: null
}
},
@@ -271,34 +238,6 @@ export default {
},
categoryLabel(c) { return { politics: '政治', porn: '色情', abuse: '辱骂', custom: '自定义' }[c] || c },
- categoryColor(c) { return { politics: '#dc3545', porn: '#e83e8c', abuse: '#fd7e14', custom: '#6c757d' }[c] || '#6c757d' },
- formatTime(t) { return t ? new Date(t).toLocaleString('zh-CN') : '' },
- formatHitWords(hw) {
- if (!hw) return ''
- // PostgreSQL JDBC 返回 JSONB 为 PGobject:{"type":"jsonb","value":"...", "null":false}
- if (hw.type === 'jsonb' && hw.value) {
- try { hw = JSON.parse(hw.value) } catch { return hw.value }
- }
- if (typeof hw === 'string') { try { hw = JSON.parse(hw) } catch { return hw } }
- // 格式:{"hits":[{"word":"xxx","level":2,"category":"politics"}]}
- if (hw && hw.hits && Array.isArray(hw.hits)) {
- return hw.hits.map(h => `${h.word || ''}(${h.category || ''})`).join('、')
- }
- if (Array.isArray(hw)) return hw.map(h => typeof h === 'string' ? h : h.word || '').join('、')
- return String(hw)
- }
- },
-
- watch: {
- showAuditLog(val) {
- if (val) {
- this.loadAuditLogs()
- this.$nextTick(() => {
- if (this.$refs.auditLogPanel) {
- this.$refs.auditLogPanel.scrollIntoView({ behavior: 'smooth', block: 'start' })
- }
- })
- }
- }
+ categoryColor(c) { return { politics: '#dc3545', porn: '#e83e8c', abuse: '#fd7e14', custom: '#6c757d' }[c] || '#6c757d' }
}
}
diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css
index 8e66e24..beb774e 100644
--- a/src/main/resources/static/css/main.css
+++ b/src/main/resources/static/css/main.css
@@ -24,19 +24,51 @@ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei"
.topbar .links { margin-left:auto; display:flex; gap:12px; }
.topbar .links a { color:#fff; opacity:.8; text-decoration:none; font-size:13px; transition:opacity .2s; }
.topbar .links a:hover { opacity:1; }
+.menu-toggle { display:none; align-items:center; justify-content:center; width:36px; height:36px; border:none; background:rgba(255,255,255,.1); color:#fff; border-radius:6px; cursor:pointer; font-size:18px; margin-right:4px; flex:none; }
-/* ==================== Tab 导航 ==================== */
-.tabs { display:flex; gap:0; background:var(--card); border-bottom:1px solid var(--border); padding:0 24px; flex:none; z-index:99; }
-.tab-btn { padding:14px 24px; border:none; background:none; font-size:14px; cursor:pointer; border-bottom:3px solid transparent; color:var(--sub); transition:all .2s; font-family:inherit; display:flex; align-items:center; gap:6px; }
-.tab-btn:hover { color:var(--text); }
-.tab-btn.active { color:var(--primary); border-bottom-color:var(--primary); font-weight:600; }
-.tab-icon { font-size:18px; }
+/* ==================== 布局主体(侧边栏 + 内容区) ==================== */
+.layout-body { display:flex; flex:1; min-height:0; overflow:hidden; }
+
+/* ==================== 侧边栏 ==================== */
+.sidebar { width:220px; flex:none; background:#1f2937; color:#e5e7eb; display:flex; flex-direction:column; transition:width .25s cubic-bezier(.4,0,.2,1); overflow:hidden; z-index:50; }
+.sidebar.collapsed { width:56px; }
+.sidebar-inner { display:flex; flex-direction:column; height:100%; overflow-y:auto; overflow-x:hidden; }
+.sidebar-nav { flex:1; padding:8px 0; }
+
+/* 菜单项 */
+.menu-item { display:flex; align-items:center; gap:10px; width:100%; padding:10px 16px; border:none; background:none; color:#d1d5db; font-size:14px; cursor:pointer; transition:background .15s, color .15s; text-align:left; font-family:inherit; white-space:nowrap; position:relative; }
+.menu-item:hover { background:rgba(255,255,255,.08); color:#fff; }
+.menu-item.active { background:rgba(255,255,255,.12); color:#fff; font-weight:600; }
+.menu-icon { font-size:18px; flex:none; width:24px; text-align:center; }
+.child-icon { font-size:15px; }
+.menu-label { flex:1; overflow:hidden; text-overflow:ellipsis; }
+.menu-arrow { font-size:12px; transition:transform .2s; flex:none; }
+.menu-arrow.expanded { transform:rotate(0deg); }
+.menu-arrow:not(.expanded) { transform:rotate(-90deg); }
+
+/* 子菜单 */
+.menu-child { padding-left:30px; font-size:13px; }
+.submenu { overflow:hidden; transition:max-height .25s ease; max-height:0; }
+.menu-group.open .submenu { max-height:300px; }
+
+/* 折叠按钮 */
+.sidebar-collapse-btn { border:none; background:rgba(255,255,255,.06); color:#9ca3af; padding:10px; cursor:pointer; font-size:12px; text-align:center; transition:background .15s; flex:none; }
+.sidebar-collapse-btn:hover { background:rgba(255,255,255,.12); color:#fff; }
+.sidebar-collapse-btn span { display:inline-block; transition:transform .25s; }
+
+/* 折叠态:仅显示图标 */
+.sidebar.collapsed .menu-label,
+.sidebar.collapsed .menu-arrow,
+.sidebar.collapsed .submenu { display:none; }
+.sidebar.collapsed .menu-item { justify-content:center; padding:12px 0; }
+.sidebar.collapsed .menu-icon { width:auto; }
/* ==================== 内容区 ==================== */
-.main { max-width:1400px; width:100%; margin:0 auto; padding:20px; flex:1; min-height:0; overflow-y:auto; }
-/* 聊天 Tab:圣杯布局,内容撑满视口、外层不滚动 */
+.main { flex:1; min-width:0; padding:20px; overflow-y:auto; }
+/* 聊天页:圣杯布局,内容撑满视口、外层不滚动 */
.main.main-chat { overflow:hidden; padding:16px; display:flex; }
-.chat-tab-pane { flex:1; min-width:0; min-height:0; display:flex; }
+.main.main-chat .page-pane { flex:1; display:flex; min-height:0; min-width:0; }
+.page-pane { min-width:0; }
@keyframes fadeIn { from{opacity:0;transform:translateY(8px);} to{opacity:1;transform:translateY(0);} }
/* ==================== 卡片 ==================== */
@@ -48,7 +80,7 @@ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei"
.input-row { display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap; }
.input, .textarea, .select { padding:10px 14px; border:1px solid var(--border); border-radius:8px; font-size:14px; font-family:inherit; outline:none; transition:border-color .2s; }
.input:focus, .textarea:focus, .select:focus { border-color:var(--primary); box-shadow:var(--ring); }
-.input { flex:1; min-width:200px; }
+.input { flex:1; min-width:200px; width:100%; }
.textarea { width:100%; resize:vertical; min-height:120px; }
.select { min-width:160px; }
.input-sm { width:120px; flex:none; }
@@ -126,13 +158,18 @@ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei"
.stat-card .label { font-size:12px; color:var(--muted); margin-top:5px; }
/* ==================== 弹窗 ==================== */
-.modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,.5); z-index:200; display:none; align-items:center; justify-content:center; }
+.modal-overlay { position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,.5); z-index:300; display:none; align-items:center; justify-content:center; }
.modal-overlay.active { display:flex; }
.modal-box { background:#fff; border-radius:var(--radius); width:90%; max-width:800px; max-height:85vh; overflow-y:auto; padding:24px; position:relative; }
.modal-box h2 { margin-bottom:16px; font-size:18px; }
.modal-close { position:absolute; top:16px; right:20px; font-size:24px; cursor:pointer; color:var(--sub); background:none; border:none; }
.modal-close:hover { color:var(--text); }
+/* 模型配置弹窗双栏网格 */
+.model-form { display:grid; grid-template-columns:1fr 1fr; gap:14px 16px; margin-top:16px; }
+.model-form-full { grid-column:1 / -1; }
+@media(max-width:640px) { .model-form { grid-template-columns:1fr; } }
+
/* ==================== 分类标签 ==================== */
.category-tag { display:inline-block; padding:2px 8px; border-radius:12px; font-size:11px; background:#f3f4f6; color:var(--text); }
@@ -155,7 +192,19 @@ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei"
.msg-item.msg-system { background:#fafafa; border-left:3px solid var(--muted); }
/* ==================== 响应式 ==================== */
-@media(max-width:768px) { .tabs { overflow-x:auto; } .tab-btn { padding:12px 16px; font-size:13px; } .stream-compare { grid-template-columns:1fr; } .stat-grid { grid-template-columns:1fr 1fr; } }
+.sidebar-overlay { display:none; }
+@media(max-width:768px) {
+ .sidebar { position:fixed; top:56px; left:0; bottom:0; width:260px; transform:translateX(-100%); transition:transform .3s cubic-bezier(.4,0,.2,1); z-index:200; }
+ .sidebar.open { transform:translateX(0); }
+ .sidebar.collapsed { width:260px; }
+ .sidebar-overlay { display:block; position:fixed; top:56px; left:0; right:0; bottom:0; background:rgba(0,0,0,.4); z-index:199; }
+ .menu-toggle { display:flex; }
+ .sidebar-collapse-btn { display:none; }
+ .main { padding:16px; }
+ .main.main-chat { padding:12px; }
+ .stream-compare { grid-template-columns:1fr; }
+ .stat-grid { grid-template-columns:1fr 1fr; }
+}
/* ==================== Chat Panel ==================== */
.chat-shell { display:grid; grid-template-columns:260px minmax(0, 1fr); gap:16px; flex:1; min-width:0; min-height:0; }
@@ -209,7 +258,7 @@ body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei"
@media(max-width:860px) {
.main.main-chat { overflow-y:auto; display:block; }
- .chat-tab-pane { display:block; }
+ .page-pane { display:block; }
.chat-shell { grid-template-columns:1fr; height:auto; min-height:0; }
.chat-sidebar { order:1; overflow:visible; }
.chat-main { order:2; min-height:560px; }
diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js
index f581b29..c03bcf3 100644
--- a/src/main/resources/static/js/app.js
+++ b/src/main/resources/static/js/app.js
@@ -1,9 +1,9 @@
/**
* Vue 应用入口
- * 组合所有组件,管理 Tab 切换和顶层布局
+ * 侧边栏导航 + 内容区布局,数据驱动菜单渲染
*/
-import { createApp, ref } from 'vue'
-import { store } from './store.js'
+import { createApp } from 'vue'
+import { store, MENU_ITEMS } from './store.js'
// 导入组件
import ChatPanel from '../components/ChatPanel.js'
@@ -19,27 +19,27 @@ 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'
const app = createApp({
setup() {
- const activeTab = ref('chat')
-
- // 切换 Tab,进入知识库 Tab 时自动加载数据
- function switchTab(tab) {
- activeTab.value = tab
- if (tab === 'document') {
- store.loadCategories()
- store.loadStats()
- } else if (tab === 'settings') {
- store.loadCategories()
- }
+ const menuItems = MENU_ITEMS
+
+ /** 判断父菜单是否高亮(任一子页面激活时高亮父菜单) */
+ function isMenuActive(parentItem) {
+ if (!parentItem.children) return store.activePage === parentItem.id
+ return parentItem.children.some(c => c.id === store.activePage)
}
- return { activeTab, switchTab, store }
+ return { menuItems, isMenuActive, store }
},
template: `
+
+
+
+
🤖 Support BotAI 智能客服系统
🧪 SDK 测试面板
@@ -47,54 +47,131 @@ const app = createApp({
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -116,5 +193,6 @@ 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.mount('#app')
diff --git a/src/main/resources/static/js/store.js b/src/main/resources/static/js/store.js
index 7af67cb..b4fb436 100644
--- a/src/main/resources/static/js/store.js
+++ b/src/main/resources/static/js/store.js
@@ -1,10 +1,49 @@
/**
* 共享响应式状态
- * 跨组件共享的分类数据、统计数据、弹窗状态
+ * 跨组件共享的分类数据、统计数据、导航状态、弹窗状态
*/
import { reactive } from 'vue'
import * as api from './api.js'
+// ==================== 菜单配置 ====================
+// 数据驱动菜单渲染,未来增删菜单只改此数组
+export const MENU_ITEMS = [
+ {
+ id: 'chat',
+ label: '智能客服对话',
+ icon: '💬'
+ },
+ {
+ id: 'knowledge',
+ label: '知识库',
+ icon: '📚',
+ children: [
+ { id: 'stats', label: '统计概览', icon: '📊' },
+ { id: 'doc-manage', label: '文档管理', icon: '📄' },
+ { id: 'category', label: '分类管理', icon: '🏷️' },
+ { id: 'search-test', label: '搜索测试', icon: '🔍' },
+ { id: 'faq', label: 'FAQ 管理', icon: '❓' }
+ ]
+ },
+ {
+ id: 'conversation',
+ label: '会话管理',
+ icon: '💬'
+ },
+ {
+ id: 'settings',
+ label: '系统设置',
+ icon: '⚙️',
+ children: [
+ { id: 'account', label: '账号管理', icon: '👤' },
+ { id: 'role', label: '角色管理', icon: '🎭' },
+ { id: 'model-config', label: '模型配置', icon: '🤖' },
+ { id: 'sensitive', label: '敏感词管理', icon: '🛡️' },
+ { id: 'audit-log', label: '审计日志', icon: '📋' }
+ ]
+ }
+]
+
export const store = reactive({
// ==================== 分类数据 ====================
categories: [],
@@ -82,5 +121,78 @@ export const store = reactive({
this.detailModal.visible = false
this.detailModal.doc = null
this.detailModal.chunks = []
+ },
+
+ // ==================== 导航状态 ====================
+ activePage: 'chat',
+ expandedMenus: ['knowledge'],
+ sidebarCollapsed: false,
+ mobileMenuOpen: false,
+
+ /**
+ * 页面跳转:切换页面 + 自动展开父菜单 + 关闭移动端菜单 + 触发数据加载
+ */
+ navigateTo(pageId) {
+ this.activePage = pageId
+ // 自动展开所属父菜单
+ for (const item of MENU_ITEMS) {
+ if (item.children && item.children.some(c => c.id === pageId)) {
+ if (!this.expandedMenus.includes(item.id)) {
+ this.expandedMenus.push(item.id)
+ }
+ }
+ }
+ // 关闭移动端菜单
+ this.mobileMenuOpen = false
+ // 页面数据加载钩子
+ this._onPageEnter(pageId)
+ },
+
+ /**
+ * 展开/折叠父菜单;折叠状态下先展开侧边栏
+ */
+ toggleMenu(menuId) {
+ if (this.sidebarCollapsed) {
+ this.sidebarCollapsed = false
+ if (!this.expandedMenus.includes(menuId)) {
+ this.expandedMenus.push(menuId)
+ }
+ return
+ }
+ const idx = this.expandedMenus.indexOf(menuId)
+ if (idx >= 0) {
+ this.expandedMenus.splice(idx, 1)
+ } else {
+ this.expandedMenus.push(menuId)
+ }
+ },
+
+ /**
+ * 侧边栏折叠/展开切换
+ */
+ toggleSidebar() {
+ this.sidebarCollapsed = !this.sidebarCollapsed
+ },
+
+ /**
+ * 页面进入时的数据加载钩子
+ */
+ _onPageEnter(pageId) {
+ switch (pageId) {
+ case 'stats':
+ case 'doc-manage':
+ case 'search-test':
+ this.loadCategories()
+ this.loadStats()
+ break
+ case 'category':
+ case 'faq':
+ this.loadCategories()
+ break
+ case 'account':
+ case 'role':
+ this.loadCategories()
+ break
+ }
}
})