- 统一移除手动创建的数据库session,统一使用models模块中的db.session - 修正项目创建接口,增加开始和结束日期的格式验证与处理 - 更新导入项目接口,使用枚举类型校验项目类型并优化异常处理 - 更新统计接口,避免多次查询假期数据,优化日期字符串处理 - 删除回滚前多余的session关闭调用,改为使用db.session.rollback() - app.py中重构数据库初始化:统一配置SQLAlchemy,动态创建数据库路径和表 - 项目模型新增开始日期和结束日期字段支持 - 添加导入批次历史记录模型支持 - 优化工具函数中日期类型提示,移除无用导入 - 更新requirements.txt依赖版本回退,确保兼容性 - 前端菜单添加导入历史导航入口,实现页面访问路由绑定
208 lines
9.5 KiB
HTML
208 lines
9.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>导入历史记录 - 个人工时记录系统</title>
|
|
<link rel="stylesheet" href="/static/css/styles.css">
|
|
</head>
|
|
<body>
|
|
<nav class="navbar">
|
|
<div class="nav-container">
|
|
<div class="nav-brand">
|
|
<h1>个人工时记录系统</h1>
|
|
</div>
|
|
<ul class="nav-menu">
|
|
<li><a href="/" class="nav-link">首页</a></li>
|
|
<li><a href="/projects" class="nav-link">项目管理</a></li>
|
|
<li><a href="/timerecords" class="nav-link">工时记录</a></li>
|
|
<li><a href="/statistics" class="nav-link">统计分析</a></li>
|
|
<li><a href="/import" class="nav-link active">导入历史</a></li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="main-content">
|
|
<div class="container">
|
|
<div class="page-header">
|
|
<h2>导入历史工时记录</h2>
|
|
</div>
|
|
|
|
<!-- 导入工具 -->
|
|
<div class="card" id="import-tool-card">
|
|
<div class="card-header">
|
|
<h3><a href="#" onclick="toggleCardBody(this); return false;" style="text-decoration: none; color: inherit;">手动导入数据 ▾</a></h3>
|
|
</div>
|
|
<div class="card-body" style="display: none;">
|
|
<p>请在下面的文本框中粘贴您的历史工时记录,每行一条。格式为:<code>月日 项目名 开始时间 结束时间 ActivityNum</code>。</p>
|
|
<p>例如:<code>8月20日 长鑫CODE/02C-FBV 9:00 17:00 9296892</code></p>
|
|
|
|
<form id="import-form">
|
|
<div class="form-group">
|
|
<label for="records-input">工时记录:</label>
|
|
<textarea id="records-input" name="records" class="form-control" rows="10" placeholder="请在此处粘贴记录..."></textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">开始导入</button>
|
|
</form>
|
|
|
|
<div id="import-results" class="import-results-container" style="display: none; margin-top: 1.5rem;">
|
|
<h4>导入结果</h4>
|
|
<p>成功导入 <strong id="success-count">0</strong> 条记录。</p>
|
|
<div id="failed-records-section" style="display: none;">
|
|
<p>以下 <strong id="failure-count">0</strong> 条记录导入失败:</p>
|
|
<ul id="failed-records-list" class="failures-list"></ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 导入历史记录 -->
|
|
<div class="history-section" style="margin-top: 2rem;">
|
|
<h3>历史记录</h3>
|
|
<div id="import-history-list" class="import-history-grid">
|
|
<!-- 历史记录卡片将由JS动态加载 -->
|
|
<p>正在加载历史记录...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<script>
|
|
// 切换卡片可见性
|
|
function toggleCardBody(element) {
|
|
const cardBody = element.closest('.card').querySelector('.card-body');
|
|
const isVisible = cardBody.style.display !== 'none';
|
|
cardBody.style.display = isVisible ? 'none' : 'block';
|
|
element.innerHTML = isVisible ? '手动导入数据 ▾' : '手动导入数据 ▴';
|
|
}
|
|
|
|
// 格式化日期
|
|
function formatImportDate(isoString) {
|
|
const date = new Date(isoString);
|
|
return date.toLocaleString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
// 加载导入历史
|
|
async function loadImportHistory() {
|
|
const listContainer = document.getElementById('import-history-list');
|
|
try {
|
|
const response = await fetch('/api/import/history');
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const history = await response.json();
|
|
|
|
if (history.length === 0) {
|
|
listContainer.innerHTML = '<p>暂无导入历史记录。</p>';
|
|
return;
|
|
}
|
|
|
|
listContainer.innerHTML = ''; // 清空加载提示
|
|
|
|
history.forEach(batch => {
|
|
let statusClass = '';
|
|
switch (batch.status) {
|
|
case '成功': statusClass = 'status-success'; break;
|
|
case '部分成功': statusClass = 'status-partial'; break;
|
|
case '失败': statusClass = 'status-fail'; break;
|
|
}
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'import-card';
|
|
card.innerHTML = `
|
|
<div class="import-card-header">
|
|
<h4>批次 #${batch.id}</h4>
|
|
<span class="import-date">${formatImportDate(batch.import_date)}</span>
|
|
</div>
|
|
<div class="import-card-body">
|
|
<div class="import-card-stats">
|
|
<div class="stat-item">
|
|
<span class="stat-value" style="color: var(--success-color);">${batch.success_count}</span>
|
|
<span class="stat-label">成功</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value" style="color: var(--danger-color);">${batch.failure_count}</span>
|
|
<span class="stat-label">失败</span>
|
|
</div>
|
|
<div class="stat-item">
|
|
<span class="stat-value">${batch.total_records}</span>
|
|
<span class="stat-label">总计</span>
|
|
</div>
|
|
</div>
|
|
<p><strong>源数据预览:</strong></p>
|
|
<div class="source-preview">${batch.source_preview || '无预览'}</div>
|
|
</div>
|
|
<div class="import-card-footer">
|
|
<span class="status-badge ${statusClass}">${batch.status}</span>
|
|
</div>
|
|
`;
|
|
listContainer.appendChild(card);
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('加载导入历史失败:', error);
|
|
listContainer.innerHTML = '<p style="color: var(--danger-color);">无法加载导入历史记录,请稍后重试。</p>';
|
|
}
|
|
}
|
|
|
|
// 处理导入表单提交
|
|
document.getElementById('import-form').addEventListener('submit', async function(event) {
|
|
event.preventDefault();
|
|
|
|
const recordsText = document.getElementById('records-input').value;
|
|
if (!recordsText.trim()) {
|
|
alert('请输入要导入的记录。');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/import', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ records: recordsText })
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
const resultsContainer = document.getElementById('import-results');
|
|
const successCount = document.getElementById('success-count');
|
|
const failedSection = document.getElementById('failed-records-section');
|
|
const failureCount = document.getElementById('failure-count');
|
|
const failedList = document.getElementById('failed-records-list');
|
|
|
|
successCount.textContent = result.success_count;
|
|
|
|
failedList.innerHTML = '';
|
|
if (result.failures && result.failures.length > 0) {
|
|
failureCount.textContent = result.failure_count;
|
|
result.failures.forEach(fail => {
|
|
const li = document.createElement('li');
|
|
li.textContent = `[${fail.reason}] ${fail.line}`;
|
|
failedList.appendChild(li);
|
|
});
|
|
failedSection.style.display = 'block';
|
|
} else {
|
|
failedSection.style.display = 'none';
|
|
}
|
|
|
|
resultsContainer.style.display = 'block';
|
|
|
|
// 导入成功后清空输入框并重新加载历史
|
|
if (result.success_count > 0) {
|
|
document.getElementById('records-input').value = '';
|
|
loadImportHistory();
|
|
}
|
|
} catch (error) {
|
|
console.error('导入请求失败:', error);
|
|
alert('导入请求失败,请检查网络连接或联系管理员。');
|
|
}
|
|
});
|
|
|
|
// 页面加载时执行
|
|
document.addEventListener('DOMContentLoaded', loadImportHistory);
|
|
</script>
|
|
</body>
|
|
</html>
|