Files
Auto-Finance/web/templates/index.html

362 lines
14 KiB
HTML
Raw 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.
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f5f7fa; }
.header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; padding: 24px 0 20px; }
.upload-zone {
border: 2px dashed #ccc; border-radius: 12px; padding: 28px; text-align: center;
cursor: pointer; transition: all .2s; background: #fff; min-height: 100px;
}
.upload-zone:hover, .upload-zone.dragover { border-color: #667eea; background: #f0f2ff; }
.upload-zone.active { border-color: #28a745; background: #f0fff4; }
.upload-zone .icon { font-size: 32px; color: #aaa; margin-bottom: 8px; }
.file-tag { display: inline-block; background: #e8f0fe; border-radius: 4px; padding: 2px 10px; margin: 3px; font-size: 13px; }
.file-tag .remove { cursor: pointer; color: #c00; margin-left: 6px; font-weight: bold; }
.log-container { background: #1e1e1e; color: #d4d4d4; border-radius: 8px; padding: 14px; height: 360px; overflow-y: auto; font-family: Consolas, monospace; font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
.log-container .empty { color: #666; font-style: italic; }
.section-title { font-size: 15px; font-weight: 600; color: #333; margin-bottom: 12px; }
.btn-process { font-size: 17px; padding: 10px 40px; }
.status-badge { font-size: 13px; }
.result-box { background: #fff; border-radius: 8px; padding: 16px; display: none; }
.result-box a { text-decoration: none; }
</style>
</head>
<body>
<div class="header text-center mb-4">
<h3>财务报销自动化</h3>
<p class="mb-0 opacity-75">上传发票 PDF 和支付截图自动提取、OCR 识别并填报</p>
</div>
<div class="container" style="max-width:960px">
<!-- 上传区域 -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="section-title">📄 发票 PDF <span class="text-muted fw-normal" style="font-size:12px">(多选)</span></div>
<div class="upload-zone" id="pdf-zone" onclick="document.getElementById('pdf-input').click()">
<div class="icon">📁</div>
<div class="text-muted" style="font-size:13px">点击或拖拽上传 PDF 文件</div>
<div id="pdf-list" class="mt-2"></div>
</div>
<input type="file" id="pdf-input" accept=".pdf" multiple hidden onchange="handleFiles(this, 'pdf')">
</div>
<div class="col-md-6">
<div class="section-title">🖼️ 支付截图 <span class="text-muted fw-normal" style="font-size:12px">(多选)</span></div>
<div class="upload-zone" id="img-zone" onclick="document.getElementById('img-input').click()">
<div class="icon">🖼️</div>
<div class="text-muted" style="font-size:13px">点击或拖拽上传图片文件</div>
<div id="img-list" class="mt-2"></div>
</div>
<input type="file" id="img-input" accept="image/*" multiple hidden onchange="handleFiles(this, 'img')">
</div>
</div>
<!-- CSV 快捷上传 -->
<div class="mb-4">
<div class="section-title">📊 CSV 快捷上传 <span class="text-muted fw-normal" style="font-size:12px">(已有发票数据 CSV 可直接上传,跳过提取和 OCR</span></div>
<div class="upload-zone" id="csv-zone" onclick="document.getElementById('csv-input').click()">
<div class="icon">📊</div>
<div class="text-muted" style="font-size:13px">点击或拖拽上传 CSV 文件</div>
<div id="csv-list" class="mt-2"></div>
</div>
<input type="file" id="csv-input" accept=".csv" hidden onchange="handleCsvFile(this)">
</div>
<!-- 配置表单 -->
<div class="card mb-4">
<div class="card-body">
<div class="section-title">⚙️ 配置
<span style="font-size:12px;font-weight:normal;cursor:pointer;color:#667eea;margin-left:8px" onclick="document.getElementById('config-upload').click()">
📤 上传 config.json
</span>
<input type="file" id="config-upload" accept=".json" hidden onchange="handleConfigUpload(this)">
</div>
<div class="row g-2">
<div class="col-sm-4">
<label class="form-label" style="font-size:12px;margin-bottom:2px">账号 (工号)</label>
<input type="text" class="form-control form-control-sm" id="cfg-username" placeholder="202xxxx">
</div>
<div class="col-sm-4">
<label class="form-label" style="font-size:12px;margin-bottom:2px">密码</label>
<input type="password" class="form-control form-control-sm" id="cfg-password" placeholder="登录密码">
</div>
<div class="col-sm-4">
<label class="form-label" style="font-size:12px;margin-bottom:2px">默认姓名</label>
<input type="text" class="form-control form-control-sm" id="cfg-name" placeholder="张三">
</div>
<div class="col-sm-3">
<label class="form-label" style="font-size:12px;margin-bottom:2px">公务卡号</label>
<input type="text" class="form-control form-control-sm" id="cfg-card" placeholder="628288...">
</div>
<div class="col-sm-3">
<label class="form-label" style="font-size:12px;margin-bottom:2px">人员编号</label>
<input type="text" class="form-control form-control-sm" id="cfg-person-id" placeholder="202xxxxx">
</div>
<div class="col-sm-6 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="cfg-submit">
<label class="form-check-label" for="cfg-submit" style="font-size:13px">同时提交到财务系统(需浏览器自动化)</label>
</div>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="text-center mb-4">
<button class="btn btn-primary btn-process" id="btn-start" onclick="startProcess()">开始处理</button>
<span id="status" class="ms-3"></span>
</div>
<!-- 结果 -->
<div class="result-box mb-3" id="result-box">
<div class="section-title">📊 处理结果</div>
<div id="result-content"></div>
</div>
<!-- 日志 -->
<div class="section-title">📋 处理日志</div>
<div class="log-container mb-4" id="log-box"><div class="empty">等待开始...</div></div>
</div>
<script>
let sessionId = null;
const pdfFiles = [], imgFiles = [];
let csvFile = null;
// ---- Session ----
async function ensureSession() {
if (sessionId) return sessionId;
const r = await fetch('/api/session', { method: 'POST' });
const d = await r.json();
sessionId = d.session_id;
return sessionId;
}
// ---- 上传 ----
function handleFiles(input, type) {
const files = Array.from(input.files);
const list = type === 'pdf' ? pdfFiles : imgFiles;
const listId = type === 'pdf' ? 'pdf-list' : 'img-list';
const zoneId = type === 'pdf' ? 'pdf-zone' : 'img-zone';
files.forEach(f => {
if (!list.find(x => x.name === f.name)) list.push(f);
});
renderFileList(type);
document.getElementById(zoneId).classList.add('active');
input.value = '';
}
function removeFile(type, index) {
const list = type === 'pdf' ? pdfFiles : imgFiles;
list.splice(index, 1);
renderFileList(type);
if (list.length === 0) {
document.getElementById(type === 'pdf' ? 'pdf-zone' : 'img-zone').classList.remove('active');
}
}
function renderFileList(type) {
const list = type === 'pdf' ? pdfFiles : imgFiles;
const box = document.getElementById(type === 'pdf' ? 'pdf-list' : 'img-list');
box.innerHTML = list.map((f, i) =>
`<span class="file-tag">${f.name}<span class="remove" onclick="event.stopPropagation();removeFile('${type}',${i})">&times;</span></span>`
).join('');
}
// ---- CSV 上传 ----
function handleCsvFile(input) {
const file = input.files[0];
if (!file) return;
csvFile = file;
document.getElementById('csv-zone').classList.add('active');
document.getElementById('csv-list').innerHTML =
`<span class="file-tag">${file.name}<span class="remove" onclick="event.stopPropagation();removeCsvFile()">&times;</span></span>`;
input.value = '';
}
function removeCsvFile() {
csvFile = null;
document.getElementById('csv-zone').classList.remove('active');
document.getElementById('csv-list').innerHTML = '';
}
// ---- 配置上传 ----
function handleConfigUpload(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const cfg = JSON.parse(e.target.result);
const map = {
'cfg-username': cfg.username,
'cfg-password': cfg.password,
'cfg-name': cfg.default_name,
'cfg-card': cfg.default_card_no,
'cfg-person-id': cfg.default_person_id,
};
for (const [id, val] of Object.entries(map)) {
if (val) document.getElementById(id).value = val;
}
alert('配置已加载');
} catch (err) {
alert('config.json 解析失败: ' + err.message);
}
};
reader.readAsText(file);
input.value = '';
}
// ---- 拖拽 ----
['pdf','img'].forEach(type => {
const zone = document.getElementById(type + '-zone');
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); });
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
zone.addEventListener('drop', e => {
e.preventDefault();
zone.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(f => {
if (type === 'pdf') return f.name.toLowerCase().endsWith('.pdf');
/\.(png|jpe?g|bmp|webp)$/i.test(f.name);
});
if (files.length) {
const list = type === 'pdf' ? pdfFiles : imgFiles;
files.forEach(f => { if (!list.find(x => x.name === f.name)) list.push(f); });
renderFileList(type);
zone.classList.add('active');
}
});
});
// CSV 拖拽
const csvZone = document.getElementById('csv-zone');
csvZone.addEventListener('dragover', e => { e.preventDefault(); csvZone.classList.add('dragover'); });
csvZone.addEventListener('dragleave', () => csvZone.classList.remove('dragover'));
csvZone.addEventListener('drop', e => {
e.preventDefault();
csvZone.classList.remove('dragover');
const file = Array.from(e.dataTransfer.files).find(f => f.name.toLowerCase().endsWith('.csv'));
if (file) handleCsvFile({ files: [file] });
});
// ---- 处理 ----
async function startProcess() {
const isCsvMode = !!csvFile;
if (!isCsvMode && !pdfFiles.length && !imgFiles.length) {
alert('请先上传文件或 CSV');
return;
}
const btn = document.getElementById('btn-start');
btn.disabled = true;
btn.textContent = '处理中...';
document.getElementById('status').innerHTML = '<span class="badge bg-warning status-badge">上传中...</span>';
document.getElementById('log-box').innerHTML = '';
document.getElementById('result-box').style.display = 'none';
try {
await ensureSession();
if (isCsvMode) {
// CSV 模式:只上传 CSV 文件
const fd = new FormData();
fd.append('file', csvFile);
await fetch(`/api/upload-csv/${sessionId}`, { method: 'POST', body: fd });
} else {
// 传统模式:上传 PDF 和图片
const allFiles = [...pdfFiles.map(f => ({f, t:'pdf'})), ...imgFiles.map(f => ({f, t:'img'}))];
for (const {f} of allFiles) {
const fd = new FormData();
fd.append('file', f);
await fetch(`/api/upload/${sessionId}`, { method: 'POST', body: fd });
}
}
document.getElementById('status').innerHTML = '<span class="badge bg-info status-badge">处理中...</span>';
// 启动管道
const cfg = {
username: document.getElementById('cfg-username').value,
password: document.getElementById('cfg-password').value,
default_name: document.getElementById('cfg-name').value,
default_card_no: document.getElementById('cfg-card').value,
default_person_id: document.getElementById('cfg-person-id').value,
submit: document.getElementById('cfg-submit').checked,
mode: isCsvMode ? 'csv' : 'auto',
};
await fetch(`/api/process/${sessionId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cfg),
});
// 监听 SSE 日志
const es = new EventSource(`/api/logs/${sessionId}`);
const logBox = document.getElementById('log-box');
let firstLine = true;
es.addEventListener('message', e => {
if (firstLine) { logBox.innerHTML = ''; firstLine = false; }
// 尝试解析 JSON完成信号否则按日志文本处理
try {
const msg = JSON.parse(e.data);
if (msg.type === 'done') {
es.close();
const result = msg.result;
document.getElementById('status').innerHTML = result.ok
? '<span class="badge bg-success status-badge">完成</span>'
: '<span class="badge bg-danger status-badge">失败</span>';
btn.disabled = false;
btn.textContent = '开始处理';
if (result.ok) {
showResult(result);
} else {
document.getElementById('result-box').style.display = 'block';
document.getElementById('result-content').innerHTML = '<span class="text-danger">' + (result.error || '处理失败') + '</span>';
}
return;
}
} catch (err) {}
logBox.innerHTML += e.data;
logBox.scrollTop = logBox.scrollHeight;
});
es.onerror = () => {
es.close();
document.getElementById('status').innerHTML = '<span class="badge bg-danger status-badge">连接中断</span>';
btn.disabled = false;
btn.textContent = '开始处理';
};
} catch (e) {
alert('请求失败: ' + (e.message || '未知错误'));
btn.disabled = false;
btn.textContent = '开始处理';
document.getElementById('status').innerHTML = '<span class="badge bg-danger status-badge">失败</span>';
}
}
function showResult(r) {
const box = document.getElementById('result-box');
box.style.display = 'block';
const mdLink = r.md_url ? `<a class="btn btn-sm btn-outline-secondary" href="${r.md_url}" download>下载 Markdown</a>` : '';
document.getElementById('result-content').innerHTML = `
<p>处理 <b>${r.invoice_count}</b> 张发票,耗时 ${r.elapsed}</p>
<a class="btn btn-sm btn-outline-primary me-2" href="${r.csv_url}" download>下载 CSV</a>
${mdLink}
`;
}
</script>
</body>
</html>