refactor(api): 重构数据库访问为SQLAlchemy绑定的session
- 统一移除手动创建的数据库session,统一使用models模块中的db.session - 修正项目创建接口,增加开始和结束日期的格式验证与处理 - 更新导入项目接口,使用枚举类型校验项目类型并优化异常处理 - 更新统计接口,避免多次查询假期数据,优化日期字符串处理 - 删除回滚前多余的session关闭调用,改为使用db.session.rollback() - app.py中重构数据库初始化:统一配置SQLAlchemy,动态创建数据库路径和表 - 项目模型新增开始日期和结束日期字段支持 - 添加导入批次历史记录模型支持 - 优化工具函数中日期类型提示,移除无用导入 - 更新requirements.txt依赖版本回退,确保兼容性 - 前端菜单添加导入历史导航入口,实现页面访问路由绑定
This commit is contained in:
386
static/js/projects.js
Normal file
386
static/js/projects.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user