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