first commit

This commit is contained in:
“yuanmengy”
2026-06-10 17:31:01 +08:00
parent a2e7fbdba3
commit 69785c214f
14 changed files with 20262 additions and 5121 deletions

View File

@@ -199,6 +199,14 @@ registerPage('kb-detail', function(container) {
return timeStr;
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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);
}