```
feat(project): 添加项目数据导出功能 - 在项目查询界面添加导出按钮,支持将项目结果导出为CSV文件 - 实现exportProjectResults函数处理导出逻辑,包括参数验证和文件下载 - 新增validateProjectFilters函数用于验证项目筛选条件 - 新增buildProjectQueryParams函数构建查询参数 - 在服务端添加/api/projects/export接口,返回CSV格式数据 - 实现CSV文件生成和下载功能,包含中文文件名处理 - 重构项目查询逻辑,提取getProjects函数复用代码 ```
This commit is contained in:
@@ -622,6 +622,7 @@
|
||||
<div style="display:flex;gap:10px;align-items:center;">
|
||||
<button class="btn btn-primary btn-sm" onclick="searchProjectResults()">查询</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="resetProjectFilters()">重置</button>
|
||||
<button class="btn btn-success btn-sm" id="btnExportProjects" onclick="exportProjectResults(this)">导出</button>
|
||||
<!-- <button class="btn btn-danger btn-sm" onclick="clearResults()">清空全部</button> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -1130,21 +1131,10 @@
|
||||
|
||||
async function loadProjectResults() {
|
||||
try {
|
||||
await loadProjectFilters();
|
||||
const params = new URLSearchParams({ page: currentProjectsPage, pageSize: 10 });
|
||||
const projectFilters = getProjectFilterValues();
|
||||
|
||||
if (projectFilters.minAmount && projectFilters.maxAmount && Number(projectFilters.minAmount) > Number(projectFilters.maxAmount)) {
|
||||
throw new Error('最小金额不能大于最大金额');
|
||||
}
|
||||
|
||||
if (projectFilters.startDate && projectFilters.endDate && projectFilters.startDate > projectFilters.endDate) {
|
||||
throw new Error('开始日期不能晚于结束日期');
|
||||
}
|
||||
|
||||
Object.entries(projectFilters).forEach(([key, value]) => {
|
||||
if (value !== '') params.set(key, value);
|
||||
});
|
||||
validateProjectFilters(projectFilters);
|
||||
await loadProjectFilters();
|
||||
const params = buildProjectQueryParams({ projectFilters, includePagination: true });
|
||||
|
||||
const res = await fetch('/api/projects?' + params);
|
||||
const json = await res.json();
|
||||
@@ -1198,6 +1188,31 @@
|
||||
};
|
||||
}
|
||||
|
||||
function validateProjectFilters(projectFilters) {
|
||||
if (projectFilters.minAmount && projectFilters.maxAmount && Number(projectFilters.minAmount) > Number(projectFilters.maxAmount)) {
|
||||
throw new Error('最小金额不能大于最大金额');
|
||||
}
|
||||
|
||||
if (projectFilters.startDate && projectFilters.endDate && projectFilters.startDate > projectFilters.endDate) {
|
||||
throw new Error('开始日期不能晚于结束日期');
|
||||
}
|
||||
}
|
||||
|
||||
function buildProjectQueryParams({ projectFilters = getProjectFilterValues(), includePagination = true } = {}) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (includePagination) {
|
||||
params.set('page', currentProjectsPage);
|
||||
params.set('pageSize', 10);
|
||||
}
|
||||
|
||||
Object.entries(projectFilters).forEach(([key, value]) => {
|
||||
if (value !== '') params.set(key, value);
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function searchProjectResults() {
|
||||
currentProjectsPage = 1;
|
||||
loadProjectResults();
|
||||
@@ -1220,6 +1235,73 @@
|
||||
loadProjectResults();
|
||||
}
|
||||
|
||||
async function exportProjectResults(buttonEl) {
|
||||
const btn = buttonEl || document.getElementById('btnExportProjects');
|
||||
const originalText = btn ? btn.textContent : '导出';
|
||||
|
||||
try {
|
||||
const projectFilters = getProjectFilterValues();
|
||||
validateProjectFilters(projectFilters);
|
||||
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '导出中...';
|
||||
}
|
||||
|
||||
const res = await fetch('/api/projects/export?' + buildProjectQueryParams({
|
||||
projectFilters,
|
||||
includePagination: false,
|
||||
}));
|
||||
|
||||
if (!res.ok) {
|
||||
let message = '导出失败';
|
||||
try {
|
||||
const json = await res.json();
|
||||
message = json.error || message;
|
||||
} catch (_error) {
|
||||
// Ignore JSON parse failure and use the fallback message.
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const downloadUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
link.download = getDownloadFilename(
|
||||
res.headers.get('content-disposition'),
|
||||
`projects-${new Date().toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '-')}.csv`
|
||||
);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
} catch (e) {
|
||||
alert('导出失败: ' + e.message);
|
||||
} finally {
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDownloadFilename(contentDisposition, fallbackName) {
|
||||
if (!contentDisposition) return fallbackName;
|
||||
|
||||
const utf8Match = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i);
|
||||
if (utf8Match && utf8Match[1]) {
|
||||
return decodeURIComponent(utf8Match[1]);
|
||||
}
|
||||
|
||||
const asciiMatch = contentDisposition.match(/filename="([^"]+)"/i) || contentDisposition.match(/filename=([^;]+)/i);
|
||||
if (asciiMatch && asciiMatch[1]) {
|
||||
return asciiMatch[1].trim();
|
||||
}
|
||||
|
||||
return fallbackName;
|
||||
}
|
||||
|
||||
function renderResults({ data, total, page, pageSize, listId, paginationId }) {
|
||||
const container = document.getElementById(listId);
|
||||
if (!data || data.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user