first commit
This commit is contained in:
@@ -199,6 +199,14 @@ registerPage('kb-detail', function(container) {
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function getStatusBadge(status) {
|
||||
const map = {
|
||||
ready: { label: '解析完成', cls: 'badge-success' },
|
||||
@@ -209,6 +217,31 @@ registerPage('kb-detail', function(container) {
|
||||
return map[status] || { label: status, cls: 'badge-default' };
|
||||
}
|
||||
|
||||
function getNowText() {
|
||||
const date = new Date();
|
||||
const pad = value => String(value).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
const value = Number(bytes) || 0;
|
||||
if (value < 1024) return `${value}B`;
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)}KB`;
|
||||
return `${(value / 1024 / 1024).toFixed(1)}MB`;
|
||||
}
|
||||
|
||||
function downloadTextFile(filename, content, mime = 'application/json;charset=utf-8') {
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
|
||||
// ==================== 渲染函数 ====================
|
||||
|
||||
function renderPage() {
|
||||
@@ -759,12 +792,66 @@ registerPage('kb-detail', function(container) {
|
||||
};
|
||||
|
||||
window.__filterSlices = function(keyword) {
|
||||
Toast.info('搜索切片: ' + keyword);
|
||||
const query = String(keyword || '').trim().toLowerCase();
|
||||
document.querySelectorAll('#sliceList .slice-list-item').forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(query) ? '' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
// 文件操作
|
||||
window.__uploadFiles = function() {
|
||||
Toast.info('上传文件功能');
|
||||
createModal({
|
||||
title: '上传文件',
|
||||
size: 'lg',
|
||||
content: `
|
||||
<div class="form-group">
|
||||
<label class="form-label">选择文件</label>
|
||||
<input type="file" class="form-control" id="detailUploadInput" multiple>
|
||||
<div class="form-hint">前端 mock 会将文件加入列表并进入“解析中”状态;后端接入后调用 POST /api/v1/knowledge-bases/:id/files。</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">所属类目</label>
|
||||
<select class="form-control" id="detailUploadCategory">
|
||||
<option value="默认类目">默认类目</option>
|
||||
<option value="教学类目">教学类目</option>
|
||||
<option value="产品文档">产品文档</option>
|
||||
<option value="用户指南">用户指南</option>
|
||||
</select>
|
||||
</div>
|
||||
`,
|
||||
confirmText: '上传',
|
||||
onConfirm: () => {
|
||||
const input = document.getElementById('detailUploadInput');
|
||||
const files = Array.from(input?.files || []);
|
||||
if (!files.length) {
|
||||
Toast.warning('请先选择文件');
|
||||
return false;
|
||||
}
|
||||
const category = document.getElementById('detailUploadCategory')?.value || '默认类目';
|
||||
files.forEach(file => {
|
||||
const ext = file.name.split('.').pop()?.toUpperCase() || 'FILE';
|
||||
kbFiles.unshift({
|
||||
id: `f-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`,
|
||||
name: file.name,
|
||||
size: formatBytes(file.size),
|
||||
format: ext,
|
||||
dataSize: formatBytes(file.size),
|
||||
status: 'processing',
|
||||
category,
|
||||
indexedAt: '-',
|
||||
slices: 0,
|
||||
chars: 0,
|
||||
parseError: null,
|
||||
});
|
||||
});
|
||||
kbData.docs += files.length;
|
||||
kbData.updatedAt = getNowText();
|
||||
selectedFiles.clear();
|
||||
renderPage();
|
||||
Toast.success(`已上传 ${files.length} 个文件`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.__viewMetaInfo = function(fileId) {
|
||||
@@ -848,7 +935,31 @@ registerPage('kb-detail', function(container) {
|
||||
};
|
||||
|
||||
window.__moveFile = function(fileId) {
|
||||
Toast.info('移动文件功能');
|
||||
const file = kbFiles.find(f => f.id === fileId);
|
||||
if (!file) {
|
||||
Toast.warning('未找到文件');
|
||||
return;
|
||||
}
|
||||
createModal({
|
||||
title: '移动到类目',
|
||||
content: `
|
||||
<p style="margin-bottom:12px">文件:<strong>${file.name}</strong></p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">目标类目</label>
|
||||
<select class="form-control" id="moveFileCategory">
|
||||
${['默认类目', '教学类目', '产品文档', '用户指南'].map(category => `
|
||||
<option value="${category}" ${file.category === category ? 'selected' : ''}>${category}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`,
|
||||
confirmText: '移动',
|
||||
onConfirm: () => {
|
||||
file.category = document.getElementById('moveFileCategory')?.value || file.category;
|
||||
renderPage();
|
||||
Toast.success('文件已移动');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.__deleteFile = function(fileId) {
|
||||
@@ -875,6 +986,10 @@ registerPage('kb-detail', function(container) {
|
||||
|
||||
// 批量操作
|
||||
window.__reparseSelectedFiles = function() {
|
||||
if (selectedFiles.size === 0) {
|
||||
Toast.warning('请先选择文件');
|
||||
return;
|
||||
}
|
||||
createModal({
|
||||
title: '重新解析',
|
||||
content: `<p>确定要重新解析选中的 <strong>${selectedFiles.size}</strong> 个文件吗?</p>`,
|
||||
@@ -893,10 +1008,43 @@ registerPage('kb-detail', function(container) {
|
||||
};
|
||||
|
||||
window.__moveSelectedFiles = function() {
|
||||
Toast.info('移动文件功能');
|
||||
if (selectedFiles.size === 0) {
|
||||
Toast.warning('请先选择文件');
|
||||
return;
|
||||
}
|
||||
createModal({
|
||||
title: '批量移动类目',
|
||||
content: `
|
||||
<p style="margin-bottom:12px">将选中的 <strong>${selectedFiles.size}</strong> 个文件移动到:</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">目标类目</label>
|
||||
<select class="form-control" id="moveSelectedCategory">
|
||||
<option value="默认类目">默认类目</option>
|
||||
<option value="教学类目">教学类目</option>
|
||||
<option value="产品文档">产品文档</option>
|
||||
<option value="用户指南">用户指南</option>
|
||||
</select>
|
||||
</div>
|
||||
`,
|
||||
confirmText: '移动',
|
||||
onConfirm: () => {
|
||||
const category = document.getElementById('moveSelectedCategory')?.value || '默认类目';
|
||||
selectedFiles.forEach(id => {
|
||||
const file = kbFiles.find(f => f.id === id);
|
||||
if (file) file.category = category;
|
||||
});
|
||||
selectedFiles.clear();
|
||||
renderPage();
|
||||
Toast.success('文件已移动');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.__deleteSelectedFiles = function() {
|
||||
if (selectedFiles.size === 0) {
|
||||
Toast.warning('请先选择文件');
|
||||
return;
|
||||
}
|
||||
createModal({
|
||||
title: '批量删除',
|
||||
content: `
|
||||
@@ -1164,7 +1312,49 @@ registerPage('kb-detail', function(container) {
|
||||
|
||||
// 知识库操作
|
||||
window.__editKb = function() {
|
||||
Toast.info('编辑知识库');
|
||||
createModal({
|
||||
title: '编辑知识库',
|
||||
size: 'lg',
|
||||
content: `
|
||||
<div class="form-group">
|
||||
<label class="form-label">知识库名称 <span style="color:var(--danger)">*</span></label>
|
||||
<input class="form-control" id="detailKbName" maxlength="64" value="${escapeHtml(kbData.name)}" placeholder="请输入知识库名称">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea class="form-control" id="detailKbDesc" rows="3" maxlength="500">${escapeHtml(kbData.description || '')}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">标签</label>
|
||||
<input class="form-control" id="detailKbTags" value="${escapeHtml(kbData.tags.join(', '))}" placeholder="多个标签用逗号分隔">
|
||||
</div>
|
||||
`,
|
||||
confirmText: '保存',
|
||||
onConfirm: () => {
|
||||
const name = document.getElementById('detailKbName')?.value.trim();
|
||||
if (!name) {
|
||||
Toast.warning('请输入知识库名称');
|
||||
return false;
|
||||
}
|
||||
kbData.name = name;
|
||||
kbData.description = document.getElementById('detailKbDesc')?.value.trim() || '';
|
||||
kbData.tags = (document.getElementById('detailKbTags')?.value || '')
|
||||
.split(/[,,]/)
|
||||
.map(tag => tag.trim())
|
||||
.filter(Boolean);
|
||||
kbData.updatedAt = getNowText();
|
||||
const catalog = Array.isArray(window.__knowledgeBaseCatalog) ? window.__knowledgeBaseCatalog : [];
|
||||
const target = catalog.find(item => item.id === kbData.id);
|
||||
if (target) Object.assign(target, {
|
||||
name: kbData.name,
|
||||
description: kbData.description,
|
||||
tags: [...kbData.tags],
|
||||
updatedAt: kbData.updatedAt,
|
||||
});
|
||||
renderPage();
|
||||
Toast.success('知识库信息已更新');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.__rebuildAllIndex = function() {
|
||||
@@ -1177,6 +1367,15 @@ registerPage('kb-detail', function(container) {
|
||||
confirmText: '重建',
|
||||
onConfirm: () => {
|
||||
// TODO: 调用 API POST /api/v1/knowledge-bases/:id/rebuild
|
||||
kbData.status = 'processing';
|
||||
kbData.updatedAt = getNowText();
|
||||
const catalog = Array.isArray(window.__knowledgeBaseCatalog) ? window.__knowledgeBaseCatalog : [];
|
||||
const target = catalog.find(item => item.id === kbData.id);
|
||||
if (target) {
|
||||
target.status = kbData.status;
|
||||
target.updatedAt = kbData.updatedAt;
|
||||
}
|
||||
renderPage();
|
||||
Toast.success('索引重建任务已启动');
|
||||
}
|
||||
});
|
||||
@@ -1191,13 +1390,75 @@ registerPage('kb-detail', function(container) {
|
||||
`,
|
||||
confirmText: '重新解析',
|
||||
onConfirm: () => {
|
||||
kbFiles = kbFiles.map(file => ({
|
||||
...file,
|
||||
status: 'processing',
|
||||
indexedAt: '-',
|
||||
parseError: null,
|
||||
}));
|
||||
kbData.status = 'processing';
|
||||
kbData.updatedAt = getNowText();
|
||||
const catalog = Array.isArray(window.__knowledgeBaseCatalog) ? window.__knowledgeBaseCatalog : [];
|
||||
const target = catalog.find(item => item.id === kbData.id);
|
||||
if (target) {
|
||||
target.status = kbData.status;
|
||||
target.updatedAt = kbData.updatedAt;
|
||||
}
|
||||
selectedFiles.clear();
|
||||
renderPage();
|
||||
Toast.success('重新解析任务已启动');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.__exportKb = function() {
|
||||
Toast.info('导出知识库');
|
||||
createModal({
|
||||
title: '导出知识库',
|
||||
content: `
|
||||
<div class="form-group">
|
||||
<label class="form-label">导出格式</label>
|
||||
<select class="form-control" id="detailExportFormat">
|
||||
<option value="json">JSON 完整数据</option>
|
||||
<option value="csv">CSV 文件列表</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="detailExportIncludeFiles" checked>
|
||||
<span>包含文件列表和切片摘要</span>
|
||||
</label>
|
||||
<div class="form-hint">浏览器会下载到默认下载目录;后端接入后替换为 GET /api/v1/knowledge-bases/:id/export。</div>
|
||||
`,
|
||||
confirmText: '确认导出',
|
||||
onConfirm: () => {
|
||||
const format = document.getElementById('detailExportFormat')?.value || 'json';
|
||||
const includeFiles = document.getElementById('detailExportIncludeFiles')?.checked !== false;
|
||||
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
if (format === 'csv') {
|
||||
const rows = [
|
||||
'id,name,format,size,status,category,slices,chars,indexedAt',
|
||||
...kbFiles.map(file => [
|
||||
file.id,
|
||||
file.name,
|
||||
file.format,
|
||||
file.size,
|
||||
file.status,
|
||||
file.category,
|
||||
file.slices,
|
||||
file.chars,
|
||||
file.indexedAt,
|
||||
].map(value => `"${String(value ?? '').replace(/"/g, '""')}"`).join(',')),
|
||||
].join('\n');
|
||||
downloadTextFile(`${kbData.id}-files-${stamp}.csv`, rows, 'text/csv;charset=utf-8');
|
||||
} else {
|
||||
downloadTextFile(`${kbData.id}-${stamp}.json`, JSON.stringify({
|
||||
knowledgeBase: kbData,
|
||||
files: includeFiles ? kbFiles : undefined,
|
||||
slices: includeFiles ? mockSlices : undefined,
|
||||
}, null, 2));
|
||||
}
|
||||
Toast.success('知识库已导出');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.__deleteKb = function() {
|
||||
@@ -1217,6 +1478,10 @@ registerPage('kb-detail', function(container) {
|
||||
confirmClass: 'btn-danger',
|
||||
onConfirm: () => {
|
||||
// TODO: 调用 API DELETE /api/v1/knowledge-bases/:id
|
||||
if (Array.isArray(window.__knowledgeBaseCatalog)) {
|
||||
const index = window.__knowledgeBaseCatalog.findIndex(item => item.id === kbData.id);
|
||||
if (index !== -1) window.__knowledgeBaseCatalog.splice(index, 1);
|
||||
}
|
||||
Toast.success('知识库已删除');
|
||||
setTimeout(() => navigateTo('knowledge-base'), 1000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user