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

386 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 项目管理页面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);
}