943 lines
35 KiB
JavaScript
943 lines
35 KiB
JavaScript
/**
|
|
* TalkingQ Platform - 知识库管理页面
|
|
* 包含知识库列表、添加知识库弹窗、筛选、分页等功能
|
|
*
|
|
* API预留:
|
|
* - GET /api/v1/knowledge-bases - 获取知识库列表
|
|
* - POST /api/v1/knowledge-bases - 创建知识库
|
|
* - DELETE /api/v1/knowledge-bases/:id - 删除知识库
|
|
* - POST /api/v1/knowledge-bases/:id/rebuild - 重建索引
|
|
*/
|
|
|
|
registerPage('knowledge-base', function(container) {
|
|
// ==================== 状态管理 ====================
|
|
let currentFilter = 'all';
|
|
let searchText = '';
|
|
let selectedRows = new Set();
|
|
let currentPage = 1;
|
|
let pageSize = 10;
|
|
let sortField = 'updatedAt';
|
|
let sortOrder = 'desc';
|
|
let isLoading = true;
|
|
let kbData = [];
|
|
|
|
// ==================== API预留 - 后端接口定义 ====================
|
|
/**
|
|
* 获取知识库列表
|
|
* GET /api/v1/knowledge-bases
|
|
* Query: { page, pageSize, filter, search, sort, order }
|
|
* Response: { list: [], total, page, pageSize }
|
|
*/
|
|
const API_GET_LIST = '/api/v1/knowledge-bases';
|
|
|
|
/**
|
|
* 删除知识库
|
|
* DELETE /api/v1/knowledge-bases/:id
|
|
* Response: { success: true }
|
|
*/
|
|
const API_DELETE = '/api/v1/knowledge-bases/:id';
|
|
|
|
/**
|
|
* 重建索引
|
|
* POST /api/v1/knowledge-bases/:id/rebuild
|
|
* Response: { success: true, taskId: string }
|
|
*/
|
|
const API_REBUILD = '/api/v1/knowledge-bases/:id/rebuild';
|
|
|
|
/**
|
|
* 批量删除
|
|
* DELETE /api/v1/knowledge-bases/batch
|
|
* Body: { ids: string[] }
|
|
* Response: { success: true, deleted: number }
|
|
*/
|
|
const API_BATCH_DELETE = '/api/v1/knowledge-bases/batch';
|
|
|
|
/**
|
|
* 导出知识库
|
|
* GET /api/v1/knowledge-bases/:id/export
|
|
* Response: 文件流
|
|
*/
|
|
const API_EXPORT = '/api/v1/knowledge-bases/:id/export';
|
|
|
|
// ==================== 模拟数据 ====================
|
|
const mockKbData = [
|
|
{ id: 'kb-001', name: '儿童科学百科', description: '包含各类儿童科学知识的百科全书', docs: 2840, size: '128MB', type: 'doc_search', typeName: '文档搜索', status: 'ready', createdAt: '2026-05-01 10:00:00', updatedAt: '2026-05-10 14:32:15', tags: ['儿童', '科学'] },
|
|
{ id: 'kb-002', name: '故事素材库 V2', description: '精选各类儿童故事素材', docs: 5200, size: '340MB', type: 'doc_search', typeName: '文档搜索', status: 'ready', createdAt: '2026-04-28 09:00:00', updatedAt: '2026-05-09 09:15:42', tags: ['故事', '儿童'] },
|
|
{ id: 'kb-003', name: '小学数学知识点', description: '小学数学知识体系整理', docs: 1560, size: '86MB', type: 'doc_search', typeName: '文档搜索', status: 'processing', createdAt: '2026-05-08 14:00:00', updatedAt: '2026-05-11 16:45:30', tags: ['数学', '教育'] },
|
|
{ id: 'kb-004', name: '英语启蒙词典', description: '儿童英语启蒙词汇', docs: 3200, size: '210MB', type: 'image_qa', typeName: '图片问答', status: 'ready', createdAt: '2026-04-20 11:00:00', updatedAt: '2026-05-08 11:20:08', tags: ['英语', '启蒙'] },
|
|
{ id: 'kb-005', name: '唐诗宋词精选', description: '经典唐诗宋词收录', docs: 890, size: '45MB', type: 'doc_search', typeName: '文档搜索', status: 'ready', createdAt: '2026-04-15 08:00:00', updatedAt: '2026-05-07 08:30:55', tags: ['诗词', '文学'] },
|
|
{ id: 'kb-006', name: '历史人物故事', description: '历史名人故事集', docs: 1200, size: '78MB', type: 'video_search', typeName: '音视频搜索', status: 'error', createdAt: '2026-04-10 16:00:00', updatedAt: '2026-05-06 14:10:22', tags: ['历史', '故事'] },
|
|
{ id: 'kb-007', name: '产品参数数据库', description: '公司产品技术参数', docs: 450, size: '32MB', type: 'data_query', typeName: '数据查询', status: 'ready', createdAt: '2026-05-05 10:00:00', updatedAt: '2026-05-11 10:00:00', tags: ['产品', '技术'] },
|
|
{ id: 'kb-008', name: '音乐教育资源库', description: '儿童音乐教育资源', docs: 560, size: '65MB', type: 'video_search', typeName: '音视频搜索', status: 'uploading', createdAt: '2026-05-12 10:00:00', updatedAt: '2026-05-12 18:00:00', tags: ['音乐', '教育'] },
|
|
];
|
|
|
|
// 初始化数据
|
|
kbData = [...mockKbData];
|
|
|
|
// ==================== 工具函数 ====================
|
|
|
|
/**
|
|
* 获取状态标签信息
|
|
*/
|
|
function getStatusBadge(status) {
|
|
const map = {
|
|
ready: { label: '已就绪', cls: 'ready' },
|
|
processing: { label: '处理中', cls: 'processing' },
|
|
uploading: { label: '上传中', cls: 'uploading' },
|
|
error: { label: '异常', cls: 'error' },
|
|
};
|
|
return map[status] || { label: status, cls: 'default' };
|
|
}
|
|
|
|
/**
|
|
* 获取类型标签信息
|
|
*/
|
|
function getTypeTag(type) {
|
|
const types = {
|
|
doc_search: { label: '文档搜索', cls: 'doc_search' },
|
|
data_query: { label: '数据查询', cls: 'data_query' },
|
|
image_qa: { label: '图片问答', cls: 'image_qa' },
|
|
video_search: { label: '音视频搜索', cls: 'video_search' },
|
|
};
|
|
return types[type] || { label: type, cls: '' };
|
|
}
|
|
|
|
/**
|
|
* 获取类型图标
|
|
*/
|
|
function getTypeIcon(type) {
|
|
const icons = {
|
|
doc_search: 'file-text',
|
|
data_query: 'table',
|
|
image_qa: 'image',
|
|
video_search: 'video'
|
|
};
|
|
return icons[type] || 'database';
|
|
}
|
|
|
|
/**
|
|
* 格式化时间
|
|
*/
|
|
function formatTime(timeStr) {
|
|
if (!timeStr) return '-';
|
|
return timeStr;
|
|
}
|
|
|
|
/**
|
|
* HTML转义
|
|
*/
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return str.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
/**
|
|
* 生成分页按钮HTML
|
|
*/
|
|
function generatePagination(current, total) {
|
|
if (total <= 1) return '';
|
|
|
|
let html = '';
|
|
const maxVisible = 5;
|
|
let start = Math.max(1, current - Math.floor(maxVisible / 2));
|
|
let end = Math.min(total, start + maxVisible - 1);
|
|
|
|
if (end - start < maxVisible - 1) {
|
|
start = Math.max(1, end - maxVisible + 1);
|
|
}
|
|
|
|
if (start > 1) {
|
|
html += `<button class="kb-pagination-btn" onclick="window.__kbGoPage(1)">1</button>`;
|
|
if (start > 2) html += `<span style="padding:0 4px;color:var(--text-muted)">...</span>`;
|
|
}
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
html += `<button class="kb-pagination-btn ${i === current ? 'active' : ''}" onclick="window.__kbGoPage(${i})">${i}</button>`;
|
|
}
|
|
|
|
if (end < total) {
|
|
if (end < total - 1) html += `<span style="padding:0 4px;color:var(--text-muted)">...</span>`;
|
|
html += `<button class="kb-pagination-btn" onclick="window.__kbGoPage(${total})">${total}</button>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
// ==================== 数据处理 ====================
|
|
|
|
/**
|
|
* 筛选数据
|
|
*/
|
|
function getFilteredData() {
|
|
let filtered = [...kbData];
|
|
|
|
// 状态筛选
|
|
if (currentFilter !== 'all') {
|
|
filtered = filtered.filter(kb => kb.status === currentFilter);
|
|
}
|
|
|
|
// 搜索筛选
|
|
if (searchText) {
|
|
const s = searchText.toLowerCase();
|
|
filtered = filtered.filter(kb =>
|
|
kb.name.toLowerCase().includes(s) ||
|
|
kb.description.toLowerCase().includes(s) ||
|
|
kb.id.toLowerCase().includes(s) ||
|
|
kb.tags.some(t => t.toLowerCase().includes(s))
|
|
);
|
|
}
|
|
|
|
// 排序
|
|
filtered.sort((a, b) => {
|
|
let valA = a[sortField];
|
|
let valB = b[sortField];
|
|
if (sortField === 'name' || sortField === 'typeName') {
|
|
return sortOrder === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
|
|
}
|
|
return sortOrder === 'asc' ? valA - valB : valB - valA;
|
|
});
|
|
|
|
return filtered;
|
|
}
|
|
|
|
// ==================== 渲染函数 ====================
|
|
|
|
/**
|
|
* 渲染页面
|
|
*/
|
|
function renderPage() {
|
|
const filteredData = getFilteredData();
|
|
const total = filteredData.length;
|
|
const totalPages = Math.ceil(total / pageSize) || 1;
|
|
const displayData = filteredData.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
|
|
// 统计
|
|
const readyCount = kbData.filter(k => k.status === 'ready').length;
|
|
const processingCount = kbData.filter(k => k.status === 'processing' || k.status === 'uploading').length;
|
|
const errorCount = kbData.filter(k => k.status === 'error').length;
|
|
|
|
container.innerHTML = `
|
|
<div class="page-container" style="padding:0">
|
|
<!-- 批量操作栏 -->
|
|
<div class="kb-batch-bar ${selectedRows.size > 0 ? 'visible' : ''}" id="batchBar">
|
|
<div class="kb-batch-info">
|
|
已选择 <strong>${selectedRows.size}</strong> 项
|
|
</div>
|
|
<div class="kb-batch-actions">
|
|
<button class="btn btn-sm btn-outline" onclick="window.__kbBatchExport()">
|
|
<i data-lucide="download" style="width:14px;height:14px"></i> 导出
|
|
</button>
|
|
<button class="btn btn-sm btn-danger" onclick="window.__kbBatchDelete()">
|
|
<i data-lucide="trash-2" style="width:14px;height:14px"></i> 删除
|
|
</button>
|
|
<button class="btn btn-sm btn-ghost" onclick="window.__kbClearSelection()">
|
|
取消选择
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 页面头部 -->
|
|
<div class="kb-list-header">
|
|
<div class="kb-list-title">
|
|
<button class="page-back-btn" data-tooltip="返回应用管理" onclick="navigateTo('agent-manage')">
|
|
<i data-lucide="arrow-left" style="width:16px;height:16px"></i>
|
|
</button>
|
|
<div>
|
|
<h1 class="page-title">知识库管理</h1>
|
|
<p class="page-desc">管理和配置您的知识库,用于 AI 检索和问答</p>
|
|
</div>
|
|
</div>
|
|
<div class="kb-list-actions">
|
|
<button class="btn btn-primary" id="addKbBtn">
|
|
<i data-lucide="plus"></i> 添加知识库
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 筛选栏 -->
|
|
<div class="filter-bar" style="justify-content:space-between">
|
|
<div class="filter-tabs">
|
|
<div class="filter-tab ${currentFilter === 'all' ? 'active' : ''}" data-filter="all" onclick="window.__kbFilter('all')">
|
|
全部 <span class="filter-count">${kbData.length}</span>
|
|
</div>
|
|
<div class="filter-tab ${currentFilter === 'ready' ? 'active' : ''}" data-filter="ready" onclick="window.__kbFilter('ready')">
|
|
已就绪 <span class="filter-count">${readyCount}</span>
|
|
</div>
|
|
<div class="filter-tab ${currentFilter === 'processing' ? 'active' : ''}" data-filter="processing" onclick="window.__kbFilter('processing')">
|
|
处理中 <span class="filter-count">${processingCount}</span>
|
|
</div>
|
|
<div class="filter-tab ${currentFilter === 'error' ? 'active' : ''}" data-filter="error" onclick="window.__kbFilter('error')">
|
|
异常 <span class="filter-count">${errorCount}</span>
|
|
</div>
|
|
</div>
|
|
<div class="filter-search">
|
|
<i data-lucide="search" style="width:15px;height:15px"></i>
|
|
<input type="text" id="kbSearchInput" placeholder="搜索知识库名称、描述、标签或ID..." value="${escapeHtml(searchText)}">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 加载状态 -->
|
|
${isLoading ? `
|
|
<div class="kb-loading">
|
|
<div class="kb-loading-spinner"></div>
|
|
<span>加载中...</span>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Skeleton加载状态 -->
|
|
${isLoading ? renderSkeleton() : ''}
|
|
|
|
<!-- 表格 -->
|
|
${!isLoading ? `
|
|
<div class="kb-table-wrapper">
|
|
<table class="kb-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width:40px">
|
|
<input type="checkbox" class="row-checkbox" id="selectAllKb" onchange="window.__kbSelectAll(this.checked)">
|
|
</th>
|
|
<th class="sortable ${sortField === 'name' ? 'sorted' : ''}" onclick="window.__kbSort('name')">
|
|
知识库名称
|
|
<i data-lucide="chevrons-up-down" class="sort-icon" style="width:12px;height:12px"></i>
|
|
</th>
|
|
<th class="sortable ${sortField === 'typeName' ? 'sorted' : ''}" onclick="window.__kbSort('typeName')">
|
|
类型
|
|
<i data-lucide="chevrons-up-down" class="sort-icon" style="width:12px;height:12px"></i>
|
|
</th>
|
|
<th class="sortable ${sortField === 'docs' ? 'sorted' : ''}" onclick="window.__kbSort('docs')">
|
|
数据量
|
|
<i data-lucide="chevrons-up-down" class="sort-icon" style="width:12px;height:12px"></i>
|
|
</th>
|
|
<th>状态</th>
|
|
<th class="sortable ${sortField === 'createdAt' ? 'sorted' : ''}" onclick="window.__kbSort('createdAt')">
|
|
创建时间
|
|
<i data-lucide="chevrons-up-down" class="sort-icon" style="width:12px;height:12px"></i>
|
|
</th>
|
|
<th class="sortable ${sortField === 'updatedAt' ? 'sorted' : ''}" onclick="window.__kbSort('updatedAt')">
|
|
更新时间
|
|
<i data-lucide="chevrons-up-down" class="sort-icon" style="width:12px;height:12px"></i>
|
|
</th>
|
|
<th style="width:220px">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${displayData.length === 0 ? `
|
|
<tr>
|
|
<td colspan="8">
|
|
${renderEmptyState()}
|
|
</td>
|
|
</tr>
|
|
` : displayData.map(kb => renderTableRow(kb)).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- 分页 -->
|
|
${total > 0 ? `
|
|
<div class="kb-pagination">
|
|
<div class="kb-pagination-info">
|
|
共 <strong>${total}</strong> 条记录,每页 ${pageSize} 条
|
|
</div>
|
|
<div class="kb-pagination-controls">
|
|
<button class="kb-pagination-btn" onclick="window.__kbGoPage(${currentPage > 1 ? currentPage - 1 : 1})" ${currentPage === 1 ? 'disabled' : ''}>
|
|
<i data-lucide="chevron-left" style="width:14px;height:14px"></i>
|
|
</button>
|
|
${generatePagination(currentPage, totalPages)}
|
|
<button class="kb-pagination-btn" onclick="window.__kbGoPage(${currentPage < totalPages ? currentPage + 1 : totalPages})" ${currentPage === totalPages ? 'disabled' : ''}>
|
|
<i data-lucide="chevron-right" style="width:14px;height:14px"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
lucide.createIcons({ elements: [container] });
|
|
bindEvents();
|
|
}
|
|
|
|
/**
|
|
* 渲染表格行
|
|
*/
|
|
function renderTableRow(kb) {
|
|
return `
|
|
<tr class="${selectedRows.has(kb.id) ? 'selected' : ''}" data-id="${kb.id}">
|
|
<td>
|
|
<input type="checkbox" class="row-checkbox" ${selectedRows.has(kb.id) ? 'checked' : ''}
|
|
onchange="window.__kbToggleSelect('${kb.id}', this.checked)">
|
|
</td>
|
|
<td>
|
|
<div class="kb-name-cell">
|
|
<div class="kb-name-icon">
|
|
<i data-lucide="${getTypeIcon(kb.type)}" style="width:18px;height:18px"></i>
|
|
</div>
|
|
<div>
|
|
<div class="kb-name-text">${escapeHtml(kb.name)}</div>
|
|
<div class="kb-name-desc">${escapeHtml(kb.description) || '暂无描述'}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="kb-type-tag ${getTypeTag(kb.type).cls}">
|
|
${getTypeTag(kb.type).label}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div style="display:flex;flex-direction:column;gap:2px">
|
|
<span>${kb.docs.toLocaleString()} 文档</span>
|
|
<span style="font-size:12px;color:var(--text-muted)">${kb.size}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="kb-status-badge ${getStatusBadge(kb.status).cls}">
|
|
<span class="status-dot"></span>
|
|
${getStatusBadge(kb.status).label}
|
|
</span>
|
|
</td>
|
|
<td style="font-size:13px;color:var(--text-secondary)">${formatTime(kb.createdAt)}</td>
|
|
<td style="font-size:13px;color:var(--text-secondary)">${formatTime(kb.updatedAt)}</td>
|
|
<td>
|
|
<div class="kb-table-actions">
|
|
<button class="btn btn-ghost btn-sm" data-tooltip="查看详情" onclick="window.__kbDetail('${kb.id}')">
|
|
<i data-lucide="eye" style="width:14px;height:14px"></i>
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm" data-tooltip="命中测试" onclick="window.__kbHitTest('${kb.id}')">
|
|
<i data-lucide="search" style="width:14px;height:14px"></i>
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm" data-tooltip="编辑" onclick="window.__kbEdit('${kb.id}')">
|
|
<i data-lucide="settings" style="width:14px;height:14px"></i>
|
|
</button>
|
|
<div class="dropdown" id="dropdown-${kb.id}">
|
|
<button class="btn btn-ghost btn-sm" data-tooltip="更多操作" onclick="window.__kbToggleDropdown('${kb.id}', this, event)">
|
|
<i data-lucide="more-horizontal" style="width:14px;height:14px"></i>
|
|
</button>
|
|
<div class="dropdown-menu" style="display:none" onclick="event.stopPropagation()">
|
|
<div class="dropdown-item" onclick="event.stopPropagation();window.__kbRebuildIndex('${kb.id}')">
|
|
<i data-lucide="refresh-cw" style="width:14px;height:14px"></i> 重建索引
|
|
</div>
|
|
<div class="dropdown-item" onclick="event.stopPropagation();window.__kbExport('${kb.id}')">
|
|
<i data-lucide="download" style="width:14px;height:14px"></i> 导出
|
|
</div>
|
|
<div class="dropdown-divider"></div>
|
|
<div class="dropdown-item danger" onclick="event.stopPropagation();window.__kbDelete('${kb.id}')">
|
|
<i data-lucide="trash-2" style="width:14px;height:14px"></i> 删除
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 渲染空状态
|
|
*/
|
|
function renderEmptyState() {
|
|
return `
|
|
<div class="kb-empty">
|
|
<div class="kb-empty-icon">
|
|
<i data-lucide="database" style="width:36px;height:36px"></i>
|
|
</div>
|
|
<div class="kb-empty-title">暂无知识库</div>
|
|
<div class="kb-empty-desc">点击上方「添加知识库」开始创建您的第一个知识库</div>
|
|
<button class="btn btn-primary" onclick="window.__showCreateKbModal()">
|
|
<i data-lucide="plus" style="width:14px;height:14px"></i> 创建知识库
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 渲染Skeleton加载状态
|
|
*/
|
|
function renderSkeleton() {
|
|
return `
|
|
<div class="kb-table-wrapper" style="padding:20px">
|
|
${[1,2,3,4,5].map(() => `
|
|
<div style="display:flex;align-items:center;gap:16px;padding:16px 0;border-bottom:1px solid var(--border)">
|
|
<div class="skeleton skeleton-avatar" style="width:18px;height:18px"></div>
|
|
<div style="flex:1">
|
|
<div class="skeleton skeleton-text" style="width:200px"></div>
|
|
<div class="skeleton skeleton-text" style="width:120px"></div>
|
|
</div>
|
|
<div class="skeleton skeleton-text" style="width:60px"></div>
|
|
<div class="skeleton skeleton-text" style="width:80px"></div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ==================== 事件绑定 ====================
|
|
|
|
function bindEvents() {
|
|
// 添加知识库
|
|
container.querySelector('#addKbBtn')?.addEventListener('click', showCreateKbModal);
|
|
|
|
// 搜索
|
|
container.querySelector('#kbSearchInput')?.addEventListener('input', debounce((e) => {
|
|
searchText = e.target.value;
|
|
currentPage = 1;
|
|
selectedRows.clear();
|
|
renderPage();
|
|
}, 300));
|
|
|
|
// 模拟加载
|
|
setTimeout(() => {
|
|
isLoading = false;
|
|
renderPage();
|
|
}, 500);
|
|
}
|
|
|
|
/**
|
|
* 防抖函数
|
|
*/
|
|
function debounce(fn, delay) {
|
|
let timer = null;
|
|
return function(...args) {
|
|
clearTimeout(timer);
|
|
timer = setTimeout(() => fn.apply(this, args), delay);
|
|
};
|
|
}
|
|
|
|
// ==================== 添加知识库弹窗 ====================
|
|
|
|
/**
|
|
* 显示创建知识库弹窗
|
|
*/
|
|
function showCreateKbModal() {
|
|
let searchValue = '';
|
|
let selectedType = 'all';
|
|
let selectedStatus = 'all';
|
|
let filteredKbData = [...kbData];
|
|
let selectedForAdd = new Set();
|
|
|
|
const renderKbList = () => {
|
|
// 应用筛选
|
|
filteredKbData = kbData.filter(kb => {
|
|
if (selectedType !== 'all' && kb.type !== selectedType) return false;
|
|
if (selectedStatus !== 'all' && kb.status !== selectedStatus) return false;
|
|
if (searchValue) {
|
|
const matchName = kb.name.toLowerCase().includes(searchValue.toLowerCase());
|
|
const matchTags = kb.tags.some(t => t.toLowerCase().includes(searchValue.toLowerCase()));
|
|
const matchDesc = kb.description?.toLowerCase().includes(searchValue.toLowerCase());
|
|
if (!matchName && !matchTags && !matchDesc) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const modalBody = document.querySelector('.kb-select-list-area');
|
|
if (!modalBody) return;
|
|
|
|
if (filteredKbData.length === 0) {
|
|
modalBody.innerHTML = `
|
|
<div class="empty-state-illustration">
|
|
<div class="empty-state-icon">
|
|
<i data-lucide="search" style="width:32px;height:32px"></i>
|
|
</div>
|
|
<div class="empty-state-title">未找到匹配的知识库</div>
|
|
<div class="empty-state-desc">尝试其他搜索关键词或筛选条件</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
modalBody.innerHTML = filteredKbData.map(kb => `
|
|
<div class="kb-select-item" data-id="${kb.id}">
|
|
<div class="kb-select-icon">
|
|
<i data-lucide="${getTypeIcon(kb.type)}" style="width:20px;height:20px"></i>
|
|
</div>
|
|
<div class="kb-select-info">
|
|
<div class="kb-select-name">
|
|
${escapeHtml(kb.name)}
|
|
${selectedForAdd.has(kb.id) ? '<span style="color:var(--primary);font-size:12px">✓ 已选择</span>' : ''}
|
|
</div>
|
|
<div class="kb-select-meta">
|
|
<span>${kb.docs.toLocaleString()} 文档</span>
|
|
<span>·</span>
|
|
<span>${kb.size}</span>
|
|
<span>·</span>
|
|
<span class="kb-type-tag ${getTypeTag(kb.type).cls}" style="padding:1px 6px;font-size:10px">${getTypeTag(kb.type).label}</span>
|
|
<span>·</span>
|
|
<span class="kb-status-badge ${getStatusBadge(kb.status).cls}" style="padding:1px 6px;font-size:10px">
|
|
<span class="status-dot" style="width:5px;height:5px"></span>
|
|
${getStatusBadge(kb.status).label}
|
|
</span>
|
|
</div>
|
|
<div class="kb-select-tags">
|
|
${kb.tags.map(t => `<span class="tag" style="font-size:10px;padding:1px 6px">${t}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="kb-select-action">
|
|
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation();window.__toggleKbSelect('${kb.id}','${escapeHtml(kb.name)}')"
|
|
${kb.status === 'processing' || kb.status === 'uploading' ? 'disabled' : ''}>
|
|
${selectedForAdd.has(kb.id) ? '取消' : (kb.status === 'processing' || kb.status === 'uploading' ? '处理中' : '选择')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 更新统计
|
|
const summary = document.querySelector('.kb-select-info-summary');
|
|
if (summary) {
|
|
summary.textContent = `共 ${filteredKbData.length} 个知识库${selectedForAdd.size > 0 ? `,已选择 ${selectedForAdd.size} 个` : ''}`;
|
|
}
|
|
|
|
lucide.createIcons({ elements: [modalBody] });
|
|
};
|
|
|
|
const modalOverlay = document.createElement('div');
|
|
modalOverlay.className = 'modal-overlay';
|
|
modalOverlay.innerHTML = `
|
|
<div class="modal kb-select-modal">
|
|
<div class="modal-header">
|
|
<span class="modal-title">添加知识库</span>
|
|
<button class="modal-close">×</button>
|
|
</div>
|
|
<div class="modal-body kb-select-modal-wrapper">
|
|
<div class="kb-select-modal-header">
|
|
<div class="kb-select-modal-filters">
|
|
<div class="filter-search" style="max-width:280px">
|
|
<i data-lucide="search" style="width:15px;height:15px"></i>
|
|
<input type="text" id="kbSelectSearch" placeholder="搜索知识库名称或标签...">
|
|
</div>
|
|
<div class="kb-select-type-filter" id="typeFilter">
|
|
<button class="kb-select-type-btn active" data-type="all">全部类型</button>
|
|
<button class="kb-select-type-btn" data-type="doc_search">文档搜索</button>
|
|
<button class="kb-select-type-btn" data-type="data_query">数据查询</button>
|
|
<button class="kb-select-type-btn" data-type="image_qa">图片问答</button>
|
|
<button class="kb-select-type-btn" data-type="video_search">音视频搜索</button>
|
|
</div>
|
|
<div class="kb-select-type-filter" id="statusFilter" style="margin-top:8px">
|
|
<button class="kb-select-type-btn active" data-status="all">全部状态</button>
|
|
<button class="kb-select-type-btn" data-status="ready">已就绪</button>
|
|
<button class="kb-select-type-btn" data-status="processing">处理中</button>
|
|
<button class="kb-select-type-btn" data-status="error">异常</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="kb-select-list-area"></div>
|
|
<div class="kb-select-modal-footer">
|
|
<div class="kb-select-info-summary">共 ${kbData.length} 个知识库</div>
|
|
<div class="kb-create-link-btn" onclick="window.__goToCreateKb()">
|
|
<i data-lucide="plus" style="width:14px;height:14px"></i>
|
|
创建新知识库
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-outline" onclick="window.__closeAddKbModal()">取消</button>
|
|
<button class="btn btn-primary" id="confirmAddKbBtn" onclick="window.__confirmAddKb()" disabled>
|
|
确认添加 <span id="selectedCountBadge">${selectedForAdd.size > 0 ? `(${selectedForAdd.size})` : ''}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(modalOverlay);
|
|
lucide.createIcons({ elements: [modalOverlay] });
|
|
|
|
const close = () => {
|
|
modalOverlay.classList.remove('open');
|
|
setTimeout(() => modalOverlay.remove(), 200);
|
|
};
|
|
|
|
// 关闭事件
|
|
modalOverlay.querySelector('.modal-close').addEventListener('click', close);
|
|
modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) close(); });
|
|
|
|
// 绑定弹窗内事件
|
|
setTimeout(() => {
|
|
// 搜索
|
|
const searchInput = document.getElementById('kbSelectSearch');
|
|
searchInput?.addEventListener('input', (e) => {
|
|
searchValue = e.target.value;
|
|
renderKbList();
|
|
});
|
|
|
|
// 类型筛选
|
|
document.querySelectorAll('#typeFilter .kb-select-type-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('#typeFilter .kb-select-type-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
selectedType = btn.dataset.type;
|
|
renderKbList();
|
|
});
|
|
});
|
|
|
|
// 状态筛选
|
|
document.querySelectorAll('#statusFilter .kb-select-type-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('#statusFilter .kb-select-type-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
selectedStatus = btn.dataset.status;
|
|
renderKbList();
|
|
});
|
|
});
|
|
|
|
renderKbList();
|
|
}, 100);
|
|
|
|
// 全局函数
|
|
window.__closeAddKbModal = close;
|
|
|
|
window.__toggleKbSelect = function(id, name) {
|
|
if (selectedForAdd.has(id)) {
|
|
selectedForAdd.delete(id);
|
|
} else {
|
|
selectedForAdd.add(id);
|
|
}
|
|
|
|
// 更新确认按钮状态
|
|
const confirmBtn = document.getElementById('confirmAddKbBtn');
|
|
const countBadge = document.getElementById('selectedCountBadge');
|
|
if (confirmBtn) {
|
|
confirmBtn.disabled = selectedForAdd.size === 0;
|
|
}
|
|
if (countBadge) {
|
|
countBadge.textContent = selectedForAdd.size > 0 ? `(${selectedForAdd.size})` : '';
|
|
}
|
|
|
|
renderKbList();
|
|
};
|
|
|
|
window.__confirmAddKb = function() {
|
|
if (selectedForAdd.size === 0) {
|
|
Toast.warning('请先选择知识库');
|
|
return;
|
|
}
|
|
|
|
const selectedNames = Array.from(selectedForAdd).map(id => {
|
|
const kb = kbData.find(k => k.id === id);
|
|
return kb ? kb.name : id;
|
|
});
|
|
|
|
Toast.success(`已添加 ${selectedForAdd.size} 个知识库到当前应用`);
|
|
close();
|
|
};
|
|
|
|
window.__goToCreateKb = function() {
|
|
close();
|
|
navigateTo('kb-create');
|
|
};
|
|
|
|
requestAnimationFrame(() => modalOverlay.classList.add('open'));
|
|
}
|
|
|
|
// ==================== 全局操作函数 ====================
|
|
|
|
// 筛选
|
|
window.__kbFilter = function(filter) {
|
|
currentFilter = filter;
|
|
currentPage = 1;
|
|
selectedRows.clear();
|
|
renderPage();
|
|
};
|
|
|
|
// 排序
|
|
window.__kbSort = function(field) {
|
|
if (sortField === field) {
|
|
sortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortField = field;
|
|
sortOrder = 'asc';
|
|
}
|
|
renderPage();
|
|
};
|
|
|
|
// 分页
|
|
window.__kbGoPage = function(page) {
|
|
const totalPages = Math.ceil(getFilteredData().length / pageSize) || 1;
|
|
if (page >= 1 && page <= totalPages) {
|
|
currentPage = page;
|
|
renderPage();
|
|
}
|
|
};
|
|
|
|
// 选择
|
|
window.__kbToggleSelect = function(id, checked) {
|
|
if (checked) {
|
|
selectedRows.add(id);
|
|
} else {
|
|
selectedRows.delete(id);
|
|
}
|
|
renderPage();
|
|
};
|
|
|
|
window.__kbSelectAll = function(checked) {
|
|
const displayData = getFilteredData().slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
if (checked) {
|
|
displayData.forEach(kb => selectedRows.add(kb.id));
|
|
} else {
|
|
selectedRows.clear();
|
|
}
|
|
renderPage();
|
|
};
|
|
|
|
window.__kbClearSelection = function() {
|
|
selectedRows.clear();
|
|
renderPage();
|
|
};
|
|
|
|
// 下拉菜单
|
|
let __kbCloseDropdownHandler = null;
|
|
|
|
window.__kbToggleDropdown = function(id, btn, e) {
|
|
if (e) e.stopPropagation();
|
|
|
|
const dropdown = btn.closest('.dropdown');
|
|
const menu = dropdown.querySelector('.dropdown-menu');
|
|
const isOpen = dropdown.classList.contains('open');
|
|
|
|
// 关闭所有下拉菜单
|
|
document.querySelectorAll('.dropdown').forEach(d => {
|
|
d.classList.remove('open');
|
|
d.querySelector('.dropdown-menu').style.display = 'none';
|
|
});
|
|
document.removeEventListener('click', __kbCloseDropdownHandler);
|
|
document.removeEventListener('mousedown', __kbCloseDropdownHandler);
|
|
|
|
if (!isOpen) {
|
|
dropdown.classList.add('open');
|
|
menu.style.display = 'block';
|
|
|
|
__kbCloseDropdownHandler = (ev) => {
|
|
if (!dropdown.contains(ev.target)) {
|
|
dropdown.classList.remove('open');
|
|
menu.style.display = 'none';
|
|
document.removeEventListener('mousedown', __kbCloseDropdownHandler);
|
|
}
|
|
};
|
|
setTimeout(() => {
|
|
document.addEventListener('mousedown', __kbCloseDropdownHandler);
|
|
}, 10);
|
|
}
|
|
};
|
|
|
|
// 操作
|
|
window.__kbDetail = function(id) {
|
|
navigateTo('kb-detail?id=' + id);
|
|
};
|
|
|
|
window.__kbHitTest = function(id) {
|
|
navigateTo('kb-detail?id=' + id + '&tab=hittest');
|
|
};
|
|
|
|
window.__kbEdit = function(id) {
|
|
Toast.info('编辑知识库:' + id);
|
|
};
|
|
|
|
/**
|
|
* 删除知识库
|
|
* API: DELETE /api/v1/knowledge-bases/:id
|
|
*/
|
|
window.__kbDelete = function(id) {
|
|
const kb = kbData.find(k => k.id === id);
|
|
createModal({
|
|
title: '删除知识库',
|
|
content: `
|
|
<p>确定要删除知识库「${kb?.name}」吗?</p>
|
|
<div class="parse-error-tip" style="margin-top:12px">
|
|
<i data-lucide="alert-triangle" style="width:16px;height:16px;flex-shrink:0"></i>
|
|
<div>
|
|
<strong>此操作不可恢复</strong>
|
|
<p style="margin-top:4px;font-size:12px">关联应用将无法继续使用该知识库,所有索引数据将被永久删除。</p>
|
|
</div>
|
|
</div>
|
|
`,
|
|
confirmText: '删除',
|
|
confirmClass: 'btn-danger',
|
|
onConfirm: () => {
|
|
// TODO: 调用 API DELETE /api/v1/knowledge-bases/:id
|
|
const idx = kbData.findIndex(k => k.id === id);
|
|
if (idx !== -1) kbData.splice(idx, 1);
|
|
renderPage();
|
|
Toast.success('知识库已删除');
|
|
}
|
|
});
|
|
lucide.createIcons();
|
|
};
|
|
|
|
/**
|
|
* 重建索引
|
|
* API: POST /api/v1/knowledge-bases/:id/rebuild
|
|
*/
|
|
window.__kbRebuildIndex = function(id) {
|
|
const kb = kbData.find(k => k.id === id);
|
|
createModal({
|
|
title: '重建索引',
|
|
content: `
|
|
<p>确定要重建知识库「${kb?.name}」的索引吗?</p>
|
|
<p style="color:var(--text-muted);font-size:13px;margin-top:8px">
|
|
重建过程中知识库可正常访问,预计需要几分钟时间。
|
|
</p>
|
|
`,
|
|
confirmText: '重建',
|
|
onConfirm: () => {
|
|
// TODO: 调用 API POST /api/v1/knowledge-bases/:id/rebuild
|
|
Toast.success('索引重建任务已启动');
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 导出知识库
|
|
* API: GET /api/v1/knowledge-bases/:id/export
|
|
*/
|
|
window.__kbExport = function(id) {
|
|
// TODO: 调用 API GET /api/v1/knowledge-bases/:id/export
|
|
Toast.info('正在导出知识库...');
|
|
};
|
|
|
|
/**
|
|
* 批量导出
|
|
* API: GET /api/v1/knowledge-bases/batch/export
|
|
*/
|
|
window.__kbBatchExport = function() {
|
|
// TODO: 调用 API GET /api/v1/knowledge-bases/batch/export
|
|
Toast.info(`正在导出 ${selectedRows.size} 个知识库...`);
|
|
};
|
|
|
|
/**
|
|
* 批量删除
|
|
* API: DELETE /api/v1/knowledge-bases/batch
|
|
*/
|
|
window.__kbBatchDelete = function() {
|
|
createModal({
|
|
title: '批量删除',
|
|
content: `
|
|
<p>确定要删除选中的 <strong>${selectedRows.size}</strong> 个知识库吗?</p>
|
|
<div class="parse-error-tip" style="margin-top:12px">
|
|
<i data-lucide="alert-triangle" style="width:16px;height:16px;flex-shrink:0"></i>
|
|
<div>
|
|
<strong>此操作不可恢复</strong>
|
|
<p style="margin-top:4px;font-size:12px">所有选中的知识库及其索引数据将被永久删除。</p>
|
|
</div>
|
|
</div>
|
|
`,
|
|
confirmText: '删除',
|
|
confirmClass: 'btn-danger',
|
|
onConfirm: () => {
|
|
// TODO: 调用 API DELETE /api/v1/knowledge-bases/batch
|
|
selectedRows.forEach(id => {
|
|
const idx = kbData.findIndex(k => k.id === id);
|
|
if (idx !== -1) kbData.splice(idx, 1);
|
|
});
|
|
selectedRows.clear();
|
|
renderPage();
|
|
Toast.success(`已删除 ${selectedRows.size + 1} 个知识库`);
|
|
}
|
|
});
|
|
};
|
|
|
|
// 导出弹窗函数
|
|
window.__showCreateKbModal = showCreateKbModal;
|
|
|
|
// ==================== 初始化 ====================
|
|
renderPage();
|
|
});
|