928 lines
29 KiB
JavaScript
928 lines
29 KiB
JavaScript
// 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();
|
||
}
|
||
|