```
feat(project): 添加项目数据导出功能 - 在项目查询界面添加导出按钮,支持将项目结果导出为CSV文件 - 实现exportProjectResults函数处理导出逻辑,包括参数验证和文件下载 - 新增validateProjectFilters函数用于验证项目筛选条件 - 新增buildProjectQueryParams函数构建查询参数 - 在服务端添加/api/projects/export接口,返回CSV格式数据 - 实现CSV文件生成和下载功能,包含中文文件名处理 - 重构项目查询逻辑,提取getProjects函数复用代码 ```
This commit is contained in:
@@ -690,12 +690,11 @@ export function listProjects({
|
||||
} = {}) {
|
||||
initResultsStore();
|
||||
|
||||
const rows = queryBaseRows({});
|
||||
const projects = buildProjectList(rows, {
|
||||
const projects = getProjects({
|
||||
dedupeByName,
|
||||
city,
|
||||
section,
|
||||
projectNameKeyword: projectName,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
@@ -713,10 +712,34 @@ export function listProjects({
|
||||
};
|
||||
}
|
||||
|
||||
export function getProjects({
|
||||
city,
|
||||
section,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
dedupeByName = true,
|
||||
} = {}) {
|
||||
initResultsStore();
|
||||
|
||||
return buildProjectList(queryBaseRows({}), {
|
||||
dedupeByName,
|
||||
city,
|
||||
section,
|
||||
projectNameKeyword: projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
|
||||
export function getProjectFilters({ dedupeByName = true } = {}) {
|
||||
initResultsStore();
|
||||
|
||||
const projects = buildProjectList(queryBaseRows({}), { dedupeByName });
|
||||
const projects = getProjects({ dedupeByName });
|
||||
const cities = [...new Set(projects.map((project) => project.city).filter(Boolean))];
|
||||
const sections = [...new Set(projects.map((project) => project.section).filter(Boolean))];
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
deleteTaskById,
|
||||
listResults,
|
||||
listProjects,
|
||||
getProjects,
|
||||
deleteResultById,
|
||||
clearResults,
|
||||
getResultFilters,
|
||||
@@ -90,6 +91,58 @@ function mergeConfigWithExistingSecrets(incoming = {}) {
|
||||
return next;
|
||||
}
|
||||
|
||||
function getProjectQueryFilters(query = {}) {
|
||||
return {
|
||||
city: query.city,
|
||||
section: query.section,
|
||||
projectName: query.projectName,
|
||||
minAmount: query.minAmount,
|
||||
maxAmount: query.maxAmount,
|
||||
startDate: query.startDate,
|
||||
endDate: query.endDate,
|
||||
};
|
||||
}
|
||||
|
||||
function formatExportTimestamp(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()),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function escapeCsvCell(value) {
|
||||
if (value === null || value === undefined) return '';
|
||||
const stringValue = String(value);
|
||||
if (/[",\r\n]/.test(stringValue)) {
|
||||
return `"${stringValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
function buildProjectsCsv(projects = []) {
|
||||
const columns = [
|
||||
['城市', (project) => project.city || ''],
|
||||
['板块', (project) => project.section || project.type || ''],
|
||||
['项目名称', (project) => project.projectName || ''],
|
||||
['金额(元)', (project) => Number.isFinite(project.amountYuan) ? project.amountYuan : ''],
|
||||
['发布日期', (project) => project.date || ''],
|
||||
['详情链接', (project) => project.detailLink || ''],
|
||||
];
|
||||
|
||||
const lines = [
|
||||
columns.map(([header]) => escapeCsvCell(header)).join(','),
|
||||
...projects.map((project) => columns.map(([, getter]) => escapeCsvCell(getter(project))).join(',')),
|
||||
];
|
||||
|
||||
return `\uFEFF${lines.join('\r\n')}`;
|
||||
}
|
||||
|
||||
let isRunning = false;
|
||||
let runningStatus = null;
|
||||
|
||||
@@ -227,25 +280,10 @@ app.get('/api/results/filters', (_req, res) => {
|
||||
|
||||
app.get('/api/projects', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
city,
|
||||
section,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
page = 1,
|
||||
pageSize = 20,
|
||||
} = req.query;
|
||||
const { page = 1, pageSize = 20 } = req.query;
|
||||
const filters = getProjectQueryFilters(req.query);
|
||||
const result = listProjects({
|
||||
city,
|
||||
section,
|
||||
projectName,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
startDate,
|
||||
endDate,
|
||||
...filters,
|
||||
page,
|
||||
pageSize,
|
||||
dedupeByName: true,
|
||||
@@ -256,6 +294,23 @@ app.get('/api/projects', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/export', (req, res) => {
|
||||
try {
|
||||
const projects = getProjects({
|
||||
...getProjectQueryFilters(req.query),
|
||||
dedupeByName: true,
|
||||
});
|
||||
const csv = buildProjectsCsv(projects);
|
||||
const filename = `projects-${formatExportTimestamp()}.csv`;
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"; filename*=UTF-8''${encodeURIComponent(filename)}`);
|
||||
res.send(csv);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/projects/filters', (_req, res) => {
|
||||
try {
|
||||
res.json({ success: true, data: getProjectFilters({ dedupeByName: true }) });
|
||||
|
||||
Reference in New Issue
Block a user