refactor(api): 重构数据库访问为SQLAlchemy绑定的session

- 统一移除手动创建的数据库session,统一使用models模块中的db.session
- 修正项目创建接口,增加开始和结束日期的格式验证与处理
- 更新导入项目接口,使用枚举类型校验项目类型并优化异常处理
- 更新统计接口,避免多次查询假期数据,优化日期字符串处理
- 删除回滚前多余的session关闭调用,改为使用db.session.rollback()
- app.py中重构数据库初始化:统一配置SQLAlchemy,动态创建数据库路径和表
- 项目模型新增开始日期和结束日期字段支持
- 添加导入批次历史记录模型支持
- 优化工具函数中日期类型提示,移除无用导入
- 更新requirements.txt依赖版本回退,确保兼容性
- 前端菜单添加导入历史导航入口,实现页面访问路由绑定
This commit is contained in:
2025-09-04 18:12:24 +08:00
parent ef9432f6da
commit 8938ce2708
29 changed files with 3490 additions and 150 deletions

386
static/js/projects.js Normal file
View File

@@ -0,0 +1,386 @@
// 项目管理页面JavaScript
let projects = [];
let currentEditingProject = null;
// 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() {
loadProjects();
setupEventListeners();
});
// 设置事件监听器
function setupEventListeners() {
// 项目表单提交
const projectForm = document.getElementById('project-form');
if (projectForm) {
projectForm.addEventListener('submit', handleProjectSubmit);
}
}
// 加载项目列表
async function loadProjects() {
try {
const response = await apiGet('/api/projects');
projects = response.data;
renderProjectsTable();
updateProjectStats();
} catch (error) {
showError('加载项目失败: ' + error.message);
console.error('加载项目失败:', error);
}
}
// 渲染项目表格
function renderProjectsTable() {
const tbody = document.getElementById('projects-tbody');
if (!tbody) return;
if (projects.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center">暂无项目数据</td></tr>';
return;
}
tbody.innerHTML = projects.map(project => `
<tr>
<td>${escapeHtml(project.project_name)}</td>
<td>
<span class="badge ${project.project_type === 'traditional' ? 'badge-primary' : 'badge-secondary'}">
${project.project_type === 'traditional' ? '传统项目' : 'PSI项目'}
</span>
</td>
<td>${escapeHtml(project.customer_name)}</td>
<td>
${project.project_type === 'traditional'
? escapeHtml(project.project_code)
: escapeHtml(project.contract_number || 'PSI-PROJ')}
</td>
<td>
<span class="badge ${project.is_active ? 'badge-success' : 'badge-danger'}">
${project.is_active ? '活跃' : '禁用'}
</span>
</td>
<td>${formatDate(project.start_date)}</td>
<td>${formatDateTime(project.created_at)}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="editProject(${project.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteProject(${project.id})">删除</button>
</td>
</tr>
`).join('');
}
// 更新项目统计
function updateProjectStats() {
const totalCount = projects.length;
const traditionalCount = projects.filter(p => p.project_type === 'traditional').length;
const psiCount = projects.filter(p => p.project_type === 'psi').length;
const totalElement = document.getElementById('total-projects-count');
const traditionalElement = document.getElementById('traditional-projects-count');
const psiElement = document.getElementById('psi-projects-count');
if (totalElement) totalElement.textContent = totalCount;
if (traditionalElement) traditionalElement.textContent = traditionalCount;
if (psiElement) psiElement.textContent = psiCount;
}
// 筛选项目
function filterProjects() {
const typeFilter = document.getElementById('project-type-filter').value;
let filteredProjects = projects;
if (typeFilter) {
filteredProjects = projects.filter(p => p.project_type === typeFilter);
}
renderFilteredProjects(filteredProjects);
}
// 渲染筛选后的项目
function renderFilteredProjects(filteredProjects) {
const tbody = document.getElementById('projects-tbody');
if (!tbody) return;
if (filteredProjects.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center">没有符合条件的项目</td></tr>';
return;
}
tbody.innerHTML = filteredProjects.map(project => `
<tr>
<td>${escapeHtml(project.project_name)}</td>
<td>
<span class="badge ${project.project_type === 'traditional' ? 'badge-primary' : 'badge-secondary'}">
${project.project_type === 'traditional' ? '传统项目' : 'PSI项目'}
</span>
</td>
<td>${escapeHtml(project.customer_name)}</td>
<td>
${project.project_type === 'traditional'
? escapeHtml(project.project_code)
: escapeHtml(project.contract_number || 'PSI-PROJ')}
</td>
<td>
<span class="badge ${project.is_active ? 'badge-success' : 'badge-danger'}">
${project.is_active ? '活跃' : '禁用'}
</span>
</td>
<td>${formatDate(project.start_date)}</td>
<td>${formatDateTime(project.created_at)}</td>
<td>
<button class="btn btn-sm btn-outline" onclick="editProject(${project.id})">编辑</button>
<button class="btn btn-sm btn-danger" onclick="deleteProject(${project.id})">删除</button>
</td>
</tr>
`).join('');
}
// 显示创建项目模态框
function showCreateProjectModal() {
currentEditingProject = null;
resetForm('project-form');
document.getElementById('modal-title').textContent = '新建项目';
// 隐藏项目类型特定字段
document.getElementById('traditional-fields').style.display = 'none';
document.getElementById('psi-fields').style.display = 'none';
showModal('create-project-modal');
}
// 项目类型切换
function toggleProjectFields() {
const projectType = document.getElementById('project_type').value;
const traditionalFields = document.getElementById('traditional-fields');
const psiFields = document.getElementById('psi-fields');
const projectCodeField = document.getElementById('project_code');
const contractNumberField = document.getElementById('contract_number');
if (projectType === 'traditional') {
traditionalFields.style.display = 'block';
psiFields.style.display = 'none';
projectCodeField.required = true;
contractNumberField.required = false;
} else if (projectType === 'psi') {
traditionalFields.style.display = 'none';
psiFields.style.display = 'block';
projectCodeField.required = false;
contractNumberField.required = true;
} else {
traditionalFields.style.display = 'none';
psiFields.style.display = 'none';
projectCodeField.required = false;
contractNumberField.required = false;
}
}
// 处理项目表单提交
async function handleProjectSubmit(e) {
e.preventDefault();
const formData = getFormData('project-form');
try {
let response;
if (currentEditingProject) {
// 更新项目
response = await apiPut(`/api/projects/${currentEditingProject.id}`, formData);
} else {
// 创建新项目
response = await apiPost('/api/projects', formData);
}
showSuccess(currentEditingProject ? '项目更新成功' : '项目创建成功');
closeModal('create-project-modal');
loadProjects(); // 重新加载项目列表
} catch (error) {
showError(error.message);
}
}
// 编辑项目
function editProject(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) {
showError('项目不存在');
return;
}
currentEditingProject = project;
document.getElementById('modal-title').textContent = '编辑项目';
// 填充表单数据
fillForm('project-form', project);
// 设置项目类型并显示对应字段
document.getElementById('project_type').value = project.project_type;
toggleProjectFields();
showModal('create-project-modal');
}
// 删除项目
async function deleteProject(projectId) {
const project = projects.find(p => p.id === projectId);
if (!project) {
showError('项目不存在');
return;
}
if (!confirm(`确定要删除项目"${project.project_name}"吗?`)) {
return;
}
try {
await apiDelete(`/api/projects/${projectId}`);
showSuccess('项目删除成功');
loadProjects(); // 重新加载项目列表
} catch (error) {
showError('删除项目失败: ' + error.message);
}
}
// 显示导入模态框
function showImportModal() {
showModal('import-modal');
}
// 下载模板文件
function downloadTemplate(type) {
let csvContent = '';
if (type === 'traditional') {
csvContent = `项目名称,项目类型,客户名,项目代码,合同号,描述
CXMT 2025 MA,traditional,长鑫存储,02C-FBV,,长鑫2025年MA项目
Project Alpha,traditional,客户A,01A-DEV,,Alpha开发项目`;
} else if (type === 'psi') {
csvContent = `项目名称,项目类型,客户名,项目代码,合同号,描述
NexChip PSI项目,psi,NexChip,PSI-PROJ,ID00462761,NexChip客户PSI项目
Samsung项目,psi,Samsung,PSI-PROJ,SC20241201,Samsung客户项目`;
} else if (type === 'mixed') {
csvContent = `项目名称,项目类型,客户名,项目代码,合同号,描述
CXMT 2025 MA,traditional,长鑫存储,02C-FBV,,长鑫2025年MA项目
NexChip PSI项目,psi,NexChip,PSI-PROJ,ID00462761,NexChip客户PSI项目
Project Beta,traditional,客户B,01B-TEST,,Beta测试项目`;
}
downloadFile(csvContent, `项目模板_${type}.csv`, 'text/csv;charset=utf-8');
}
// 导入项目
async function importProjects() {
const fileInput = document.getElementById('import-file');
const file = fileInput.files[0];
if (!file) {
showError('请选择CSV文件');
return;
}
if (!file.name.endsWith('.csv')) {
showError('请选择CSV文件');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/api/projects/import', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || '导入失败');
}
// 显示导入结果
showImportResult(result);
closeModal('import-modal');
loadProjects(); // 重新加载项目列表
} catch (error) {
showError('导入项目失败: ' + error.message);
}
}
// 显示导入结果
function showImportResult(result) {
const content = document.getElementById('import-result-content');
let html = `<div class="import-summary">
<h4>导入完成</h4>
<p>成功导入 <strong>${result.created_count}</strong> 个项目</p>
</div>`;
if (result.errors && result.errors.length > 0) {
html += `<div class="import-errors">
<h5>导入错误 (${result.errors.length} 项):</h5>
<ul>`;
result.errors.forEach(error => {
html += `<li>${escapeHtml(error)}</li>`;
});
html += `</ul></div>`;
}
content.innerHTML = html;
showModal('import-result-modal');
}
// HTML转义函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 添加徽章样式如果CSS中没有定义
if (!document.querySelector('#badge-styles')) {
const style = document.createElement('style');
style.id = 'badge-styles';
style.textContent = `
.badge {
display: inline-block;
padding: 4px 8px;
font-size: 12px;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 4px;
}
.badge-primary {
background-color: #3498db;
color: white;
}
.badge-secondary {
background-color: #6c757d;
color: white;
}
.badge-success {
background-color: #27ae60;
color: white;
}
.badge-danger {
background-color: #e74c3c;
color: white;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
`;
document.head.appendChild(style);
}