text-classify-ui/static/js/dashboard.js
superlishunqin f434b83090 first commit
2025-03-17 22:43:53 +08:00

928 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// static/js/dashboard.js
// 全局变量和状态管理
const state = {
currentPage: 'classify', // 当前显示的页面
documentsData: { // 文档列表数据
items: [], // 当前页的文档条目
pagination: { // 分页信息
total: 0,
per_page: 10,
current_page: 1,
total_pages: 0
},
categories: [], // 所有类别
currentCategory: 'all', // 当前选择的类别
selectedIds: [] // 选中的文档ID
}
};
// DOM元素缓存
const elements = {
// 侧边栏和导航
sidebar: document.querySelector('.sidebar'),
sidebarToggle: document.getElementById('sidebar-toggle'),
sidebarLinks: document.querySelectorAll('.sidebar-link'),
mainContent: document.querySelector('.main-content'),
// 页面sections
pageSections: document.querySelectorAll('.page-section'),
classifySection: document.getElementById('classify-section'),
documentsSection: document.getElementById('documents-section'),
batchSection: document.getElementById('batch-section'),
// 文本分类相关
textInput: document.getElementById('text-input'),
classifyTextBtn: document.getElementById('classify-text-btn'),
clearTextBtn: document.getElementById('clear-text-btn'),
classificationResult: document.getElementById('classification-result'),
fileUpload: document.getElementById('file-upload'),
uploadFileBtn: document.getElementById('upload-file-btn'),
fileResult: document.getElementById('file-result'),
// 已处理文本相关
categoryFilter: document.getElementById('category-filter'),
perPageSelect: document.getElementById('per-page-select'),
downloadSelectedBtn: document.getElementById('download-selected-btn'),
documentsTable: document.getElementById('documents-table'),
selectAllDocs: document.getElementById('select-all-docs'),
paginationContainer: document.getElementById('pagination-container'),
// 批量处理相关
batchFileUpload: document.getElementById('batch-file-upload'),
uploadBatchBtn: document.getElementById('upload-batch-btn'),
batchResult: document.getElementById('batch-result'),
// 加载指示器
loadingOverlay: document.getElementById('loading-overlay')
};
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 绑定事件
bindEvents();
// 初始化分类模型
initializeModel();
});
// 事件绑定
function bindEvents() {
// 侧边栏切换
elements.sidebarToggle?.addEventListener('click', toggleSidebar);
// 侧边栏导航链接
elements.sidebarLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const targetPage = link.dataset.page;
navigateToPage(targetPage);
});
});
// 文本分类相关
elements.classifyTextBtn?.addEventListener('click', classifyTextContent);
elements.clearTextBtn?.addEventListener('click', clearTextContent);
elements.uploadFileBtn?.addEventListener('click', uploadAndClassifyFile);
// 已处理文本相关
elements.categoryFilter?.addEventListener('change', filterDocumentsByCategory);
elements.perPageSelect?.addEventListener('change', changePageSize);
elements.selectAllDocs?.addEventListener('change', toggleSelectAllDocuments);
elements.downloadSelectedBtn?.addEventListener('click', downloadSelectedDocuments);
// 批量处理相关
elements.uploadBatchBtn?.addEventListener('click', uploadAndClassifyBatch);
}
// 侧边栏切换
function toggleSidebar() {
elements.sidebar.classList.toggle('active');
elements.mainContent.classList.toggle('sidebar-active');
}
// 页面导航
function navigateToPage(targetPage) {
// 保存当前页面
state.currentPage = targetPage;
// 更新活动链接
elements.sidebarLinks.forEach(link => {
if (link.dataset.page === targetPage) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
// 显示对应页面
elements.pageSections.forEach(section => {
if (section.id === `${targetPage}-section`) {
section.classList.remove('d-none');
} else {
section.classList.add('d-none');
}
});
// 加载页面特定数据
if (targetPage === 'documents') {
loadDocuments();
}
}
// 显示加载指示器
function showLoading() {
elements.loadingOverlay.classList.add('show');
}
// 隐藏加载指示器
function hideLoading() {
elements.loadingOverlay.classList.remove('show');
}
// API请求公共方法
async function apiRequest(url, options = {}) {
try {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '请求失败');
}
return data;
} catch (error) {
console.error('API请求失败:', error);
throw error;
}
}
// 初始化分类模型
async function initializeModel() {
try {
await apiRequest('/api/classify/categories');
console.log('模型加载成功');
} catch (error) {
console.error('模型初始化失败:', error);
alert('模型加载失败,请刷新页面重试');
}
}
// 文本分类函数
async function classifyTextContent() {
const text = elements.textInput.value.trim();
if (!text) {
alert('请输入需要分类的文本');
return;
}
showLoading();
try {
const result = await apiRequest('/api/classify/classify-text', {
method: 'POST',
body: JSON.stringify({ text })
});
// 显示分类结果
displayTextClassificationResult(result);
} catch (error) {
elements.classificationResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
分类失败: ${error.message}
</div>
`;
elements.classificationResult.classList.remove('result-success');
elements.classificationResult.classList.add('result-error');
} finally {
hideLoading();
}
}
// 清空文本内容
function clearTextContent() {
elements.textInput.value = '';
elements.classificationResult.innerHTML = `
<h5 class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>
分类结果将在这里显示
</h5>
<div class="text-center mt-4">
<i class="fas fa-arrow-left text-muted me-2"></i>
请在左侧输入文本或上传文件
</div>
`;
elements.classificationResult.classList.remove('result-success', 'result-error');
}
// 显示文本分类结果
function displayTextClassificationResult(result) {
if (!result.success) {
elements.classificationResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
分类失败: ${result.error}
</div>
`;
elements.classificationResult.classList.remove('result-success');
elements.classificationResult.classList.add('result-error');
return;
}
// 获取所有类别的置信度,并排序
const confidences = Object.entries(result.all_confidences)
.map(([category, value]) => ({ category, value }))
.sort((a, b) => b.value - a.value);
// 创建置信度条形图HTML
const confidenceBarsHtml = confidences.slice(0, 5).map(item => {
const percentage = (item.value * 100).toFixed(2);
return `
<div class="mb-2">
<div class="d-flex justify-content-between align-items-center">
<span>${item.category}</span>
<span>${percentage}%</span>
</div>
<div class="progress confidence-bar">
<div class="progress-bar ${item.category === result.category ? 'bg-success' : 'bg-primary'}"
role="progressbar"
style="width: ${percentage}%"
aria-valuenow="${percentage}"
aria-valuemin="0"
aria-valuemax="100">
</div>
</div>
</div>
`;
}).join('');
// 更新结果显示
elements.classificationResult.innerHTML = `
<div class="text-center mb-4">
<div class="display-4 fw-bold text-success">${result.category}</div>
<div class="text-muted">置信度: ${(result.confidence * 100).toFixed(2)}%</div>
</div>
<h5 class="mb-3">类别置信度分布:</h5>
${confidenceBarsHtml}
<div class="alert alert-info mt-4">
<i class="fas fa-info-circle me-2"></i>
该文本被成功分类为 <strong>${result.category}</strong> 类别
</div>
`;
elements.classificationResult.classList.add('result-success');
elements.classificationResult.classList.remove('result-error');
}
// 上传并分类文件
async function uploadAndClassifyFile() {
const fileInput = elements.fileUpload;
if (!fileInput.files || fileInput.files.length === 0) {
alert('请选择要上传的文件');
return;
}
const file = fileInput.files[0];
// 检查文件类型
if (!file.name.toLowerCase().endsWith('.txt')) {
alert('只支持上传 .txt 格式的文本文件');
return;
}
showLoading();
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/classify/single', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '文件分类失败');
}
// 显示文件分类结果
displayFileClassificationResult(result);
} catch (error) {
elements.fileResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
文件分类失败: ${error.message}
</div>
`;
elements.fileResult.style.display = 'block';
} finally {
hideLoading();
}
}
// static/js/dashboard.js (继续)
// 显示文件分类结果
function displayFileClassificationResult(result) {
elements.fileResult.innerHTML = `
<div class="alert alert-success">
<div class="d-flex justify-content-between align-items-center">
<div>
<i class="fas fa-check-circle me-2"></i>
文件 <strong>${result.filename}</strong> 已成功分类为 <strong>${result.category}</strong> 类别
</div>
<div>
<span class="badge bg-success">${(result.confidence * 100).toFixed(2)}%</span>
</div>
</div>
</div>
<div class="mt-2">
<div class="text-muted mb-2">文件已保存为: ${result.stored_filename}</div>
<button class="btn btn-sm btn-outline-primary view-documents-btn">
<i class="fas fa-list me-1"></i> 查看已处理文档
</button>
</div>
`;
elements.fileResult.style.display = 'block';
// 添加查看已处理文档按钮事件
document.querySelector('.view-documents-btn')?.addEventListener('click', () => {
navigateToPage('documents');
});
// 清空文件上传控件
elements.fileUpload.value = '';
}
// 上传并批量分类文件
async function uploadAndClassifyBatch() {
const fileInput = elements.batchFileUpload;
if (!fileInput.files || fileInput.files.length === 0) {
alert('请选择要上传的压缩文件');
return;
}
const file = fileInput.files[0];
// 检查文件类型
const fileName = file.name.toLowerCase();
if (!fileName.endsWith('.zip') && !fileName.endsWith('.rar')) {
alert('只支持上传 .zip 或 .rar 格式的压缩文件');
return;
}
// 检查文件大小
if (file.size > 10 * 1024 * 1024) { // 10MB
alert('文件大小不能超过10MB');
return;
}
showLoading();
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/classify/batch', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error || '批量分类失败');
}
// 显示批量分类结果
displayBatchClassificationResult(result);
} catch (error) {
elements.batchResult.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
批量分类失败: ${error.message}
</div>
`;
elements.batchResult.style.display = 'block';
} finally {
hideLoading();
// 清空文件上传控件
elements.batchFileUpload.value = '';
}
}
// 显示批量分类结果
function displayBatchClassificationResult(result) {
const { archive_name, results } = result;
// 计算成功率
const successRate = results.total > 0 ? ((results.success / results.total) * 100).toFixed(1) : 0;
// 构建类别分布HTML
let categoriesHtml = '';
if (results.categories && Object.keys(results.categories).length > 0) {
categoriesHtml = `
<div class="mt-4">
<h5>分类类别分布:</h5>
<div class="row">
${Object.entries(results.categories).map(([category, count]) => `
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stat-card bg-light">
<div class="card-body p-3">
<h6 class="card-title">${category}</h6>
<div class="stat-value">${count}</div>
<div class="text-muted">文件数</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
// 构建失败文件列表HTML
let failedFilesHtml = '';
if (results.failed > 0 && results.failed_files && results.failed_files.length > 0) {
failedFilesHtml = `
<div class="mt-4">
<h5>处理失败的文件:</h5>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>文件名</th>
<th>错误原因</th>
</tr>
</thead>
<tbody>
${results.failed_files.map(file => `
<tr>
<td>${file.filename}</td>
<td>${file.error}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
`;
}
// 构建完整的结果HTML
elements.batchResult.innerHTML = `
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
压缩包 <strong>${archive_name}</strong> 的处理结果
</div>
<div class="row text-center mt-4">
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stat-card bg-light">
<div class="card-body">
<h6 class="card-title">总文件数</h6>
<div class="stat-value">${results.total}</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stat-card bg-success text-white">
<div class="card-body">
<h6 class="card-title">成功处理</h6>
<div class="stat-value">${results.success}</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stat-card ${results.failed > 0 ? 'bg-danger text-white' : 'bg-light'}">
<div class="card-body">
<h6 class="card-title">处理失败</h6>
<div class="stat-value">${results.failed}</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 mb-3">
<div class="card stat-card bg-info text-white">
<div class="card-body">
<h6 class="card-title">成功率</h6>
<div class="stat-value">${successRate}%</div>
</div>
</div>
</div>
</div>
${categoriesHtml}
${failedFilesHtml}
<div class="mt-4 text-center">
<button class="btn btn-primary view-documents-btn">
<i class="fas fa-list me-1"></i> 查看所有已处理文档
</button>
</div>
`;
elements.batchResult.style.display = 'block';
// 添加查看已处理文档按钮事件
document.querySelector('#batch-result .view-documents-btn')?.addEventListener('click', () => {
navigateToPage('documents');
});
}
// 加载已分类文档列表
async function loadDocuments() {
showLoading();
try {
const category = state.documentsData.currentCategory;
const page = state.documentsData.pagination.current_page;
const perPage = state.documentsData.pagination.per_page;
const result = await apiRequest(`/api/classify/documents?category=${category}&page=${page}&per_page=${perPage}`);
if (!result.success) {
throw new Error(result.error || '加载文档失败');
}
// 更新状态
state.documentsData.items = result.documents;
state.documentsData.pagination = result.pagination;
state.documentsData.categories = result.categories;
state.documentsData.currentCategory = result.current_category;
// 更新类别筛选下拉菜单
updateCategoryFilter(result.categories, result.current_category);
// 更新文档表格
updateDocumentsTable(result.documents);
// 更新分页控件
updatePagination(result.pagination);
// 重置选中状态
state.documentsData.selectedIds = [];
updateDownloadButtonState();
} catch (error) {
console.error('加载文档失败:', error);
elements.documentsTable.querySelector('tbody').innerHTML = `
<tr>
<td colspan="6" class="text-center py-4 text-danger">
<i class="fas fa-exclamation-circle me-2"></i>
加载文档失败: ${error.message}
</td>
</tr>
`;
} finally {
hideLoading();
}
}
// 更新类别筛选下拉菜单
function updateCategoryFilter(categories, currentCategory) {
const select = elements.categoryFilter;
// 保存第一个全部选项
const allOption = select.querySelector('option[value="all"]');
// 清空现有选项
select.innerHTML = '';
// 添加全部选项
select.appendChild(allOption);
// 添加类别选项
categories.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
select.appendChild(option);
});
// 设置当前选中值
select.value = currentCategory;
}
// 更新文档表格
function updateDocumentsTable(documents) {
const tbody = elements.documentsTable.querySelector('tbody');
if (!documents || documents.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<i class="fas fa-folder-open me-2"></i>
没有找到文档
</td>
</tr>
`;
return;
}
tbody.innerHTML = documents.map(doc => {
// 格式化文件大小
const fileSize = formatFileSize(doc.file_size);
// 格式化日期
const classifiedDate = new Date(doc.classified_time).toLocaleString('zh-CN');
return `
<tr>
<td>
<div class="form-check">
<input class="form-check-input document-checkbox" type="checkbox" value="${doc.id}" data-id="${doc.id}">
</div>
</td>
<td>${doc.original_filename}</td>
<td>
<span class="badge category-badge bg-primary">${doc.category}</span>
</td>
<td class="file-size">${fileSize}</td>
<td>${classifiedDate}</td>
<td>
<a href="/api/classify/download/${doc.id}" class="btn btn-sm btn-outline-primary file-action-btn">
<i class="fas fa-download"></i>
</a>
</td>
</tr>
`;
}).join('');
// 添加复选框事件监听
tbody.querySelectorAll('.document-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', handleDocumentCheckboxChange);
});
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 更新分页控件
function updatePagination(pagination) {
const { total, per_page, current_page, total_pages } = pagination;
if (total_pages <= 1) {
elements.paginationContainer.innerHTML = '';
return;
}
// 构建分页按钮
let paginationHtml = `
<div>
显示 ${total} 条记录中的 ${Math.min((current_page - 1) * per_page + 1, total)}${Math.min(current_page * per_page, total)}
</div>
<ul class="pagination">
`;
// 上一页按钮
paginationHtml += `
<li class="page-item ${current_page === 1 ? 'disabled' : ''}">
<a class="page-link" href="#" data-page="${current_page - 1}" aria-label="上一页">
<i class="fas fa-chevron-left"></i>
</a>
</li>
`;
// 页码按钮
const maxVisiblePages = 5;
let startPage = Math.max(1, current_page - Math.floor(maxVisiblePages / 2));
let endPage = Math.min(total_pages, startPage + maxVisiblePages - 1);
// 调整startPage以显示maxVisiblePages个页码
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
// 第一页
if (startPage > 1) {
paginationHtml += `
<li class="page-item">
<a class="page-link" href="#" data-page="1">1</a>
</li>
`;
if (startPage > 2) {
paginationHtml += `
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
`;
}
}
// 中间页码
for (let i = startPage; i <= endPage; i++) {
paginationHtml += `
<li class="page-item ${i === current_page ? 'active' : ''}">
<a class="page-link" href="#" data-page="${i}">${i}</a>
</li>
`;
}
// 最后页
if (endPage < total_pages) {
if (endPage < total_pages - 1) {
paginationHtml += `
<li class="page-item disabled">
<a class="page-link" href="#">...</a>
</li>
`;
}
paginationHtml += `
<li class="page-item">
<a class="page-link" href="#" data-page="${total_pages}">${total_pages}</a>
</li>
`;
}
// 下一页按钮
paginationHtml += `
<li class="page-item ${current_page === total_pages ? 'disabled' : ''}">
<a class="page-link" href="#" data-page="${current_page + 1}" aria-label="下一页">
<i class="fas fa-chevron-right"></i>
</a>
</li>
`;
paginationHtml += '</ul>';
// 更新分页控件
elements.paginationContainer.innerHTML = paginationHtml;
// 添加分页点击事件
elements.paginationContainer.querySelectorAll('.page-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
if (this.parentElement.classList.contains('disabled')) {
return;
}
const page = parseInt(this.dataset.page, 10);
if (page && page !== current_page) {
state.documentsData.pagination.current_page = page;
loadDocuments();
}
});
});
}
// 处理文档复选框变化
function handleDocumentCheckboxChange(e) {
const checkbox = e.target;
const documentId = parseInt(checkbox.dataset.id, 10);
if (checkbox.checked) {
// 添加到选中列表
if (!state.documentsData.selectedIds.includes(documentId)) {
state.documentsData.selectedIds.push(documentId);
}
} else {
// 从选中列表移除
state.documentsData.selectedIds = state.documentsData.selectedIds.filter(id => id !== documentId);
// 取消全选复选框
elements.selectAllDocs.checked = false;
}
// 更新下载按钮状态
updateDownloadButtonState();
}
// 全选/取消全选文档
function toggleSelectAllDocuments(e) {
const isChecked = e.target.checked;
// 获取当前页面所有文档复选框
const checkboxes = elements.documentsTable.querySelectorAll('.document-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = isChecked;
const documentId = parseInt(checkbox.dataset.id, 10);
if (isChecked) {
// 添加到选中列表
if (!state.documentsData.selectedIds.includes(documentId)) {
state.documentsData.selectedIds.push(documentId);
}
} else {
// 从选中列表移除
state.documentsData.selectedIds = state.documentsData.selectedIds.filter(id => id !== documentId);
}
});
// 更新下载按钮状态
updateDownloadButtonState();
}
// 更新下载按钮状态
function updateDownloadButtonState() {
const downloadBtn = elements.downloadSelectedBtn;
if (state.documentsData.selectedIds.length > 0) {
downloadBtn.classList.remove('disabled');
downloadBtn.textContent = `下载选中文件 (${state.documentsData.selectedIds.length})`;
} else {
downloadBtn.classList.add('disabled');
downloadBtn.innerHTML = '<i class="fas fa-download me-1"></i> 下载选中文件';
}
}
// 下载选中文档
async function downloadSelectedDocuments() {
if (state.documentsData.selectedIds.length === 0) {
return;
}
showLoading();
try {
// 单个文件直接下载多个文件使用API下载压缩包
if (state.documentsData.selectedIds.length === 1) {
const documentId = state.documentsData.selectedIds[0];
window.location.href = `/api/classify/download/${documentId}`;
hideLoading();
return;
}
// 多个文件,发送批量下载请求
const response = await fetch('/api/classify/download-multiple', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
document_ids: state.documentsData.selectedIds
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '下载失败');
}
// 获取blob数据
const blob = await response.blob();
// 创建下载链接
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `documents_${new Date().getTime()}.zip`;
document.body.appendChild(a);
a.click();
// 清理
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('下载文档失败:', error);
alert(`下载文档失败: ${error.message}`);
} finally {
hideLoading();
}
}
// 按类别筛选文档
function filterDocumentsByCategory(e) {
const category = e.target.value;
state.documentsData.currentCategory = category;
state.documentsData.pagination.current_page = 1;
loadDocuments();
}
// 修改每页显示数量
function changePageSize(e) {
const perPage = parseInt(e.target.value, 10);
state.documentsData.pagination.per_page = perPage;
state.documentsData.pagination.current_page = 1;
loadDocuments();
}