Files
TalkingQ_Dashboard/scripts/knowledge-base.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
/**
* 生成分页按钮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">&times;</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();
});