pradsa / templates /dsa_batch.html
plokmii's picture
Upload templates/dsa_batch.html with huggingface_hub
b046626 verified
Raw
History Blame Contribute Delete
8.52 kB
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DSA 批次新增資料</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background: #f5f7fa; }
.drop-zone { border: 2px dashed #adb5bd; border-radius: 12px; padding: 2rem; text-align: center; cursor: pointer; background: #fff; }
.drop-zone:hover, .drop-zone.dragover { border-color: #0d6efd; background: #e8f0fe; }
table.preview td, table.preview th { font-size: .88rem; }
.status-ok { color: #198754; font-weight: bold; }
.status-err { color: #dc3545; font-weight: bold; }
.status-dup { color: #fd7e14; font-weight: bold; }
</style>
</head>
<body>
<div class="container py-4" style="max-width: 1100px;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="mb-0">DSA 批次新增資料</h2>
<div>
<a href="/" class="btn btn-dark btn-sm me-1">首頁</a>
<a href="/dsa/history" class="btn btn-outline-secondary btn-sm me-1">歷史紀錄</a>
<a href="/dsa/new" class="btn btn-outline-primary btn-sm">單筆新增</a>
</div>
</div>
<div class="alert alert-info">
<strong>檔名格式</strong>:必須包含<strong>病歷號</strong><strong>姓名</strong>,用底線 <code>_</code> 或空格分隔。<br>
範例:<code>15713765 黃小明 LS1.xls</code><code>40091041_王小明_LS2_20240605.xls</code><br>
系統會自動抓取,日期與 LS1 / LS2 / SA 之類描述會被忽略。
</div>
<div class="card mb-3">
<div class="card-body">
<div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()">
<div style="font-size:2rem;">&#128193;</div>
<div class="fw-bold">點擊或拖曳選取多個 XLS 檔</div>
<input type="file" id="fileInput" multiple accept=".xls,.xlsx" style="display:none" onchange="onPick(this)">
</div>
</div>
</div>
<div id="previewWrap" style="display:none;">
<h5>預覽 <span class="text-muted small">(可手動修正病歷號 / 姓名)</span></h5>
<table class="table table-bordered table-sm bg-white preview">
<thead class="table-light">
<tr><th>檔名</th><th style="width:140px;">病歷號</th><th style="width:140px;">姓名</th><th style="width:100px;">狀態</th><th style="width:72px;"></th></tr>
</thead>
<tbody id="previewBody"></tbody>
</table>
<div class="mb-3">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="dupmode" id="mode-overwrite" value="overwrite">
<label class="form-check-label" for="mode-overwrite">覆寫</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="dupmode" id="mode-new" value="new" checked>
<label class="form-check-label" for="mode-new">可多筆存在</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="dupmode" id="mode-skip" value="skip">
<label class="form-check-label" for="mode-skip">跳過</label>
</div>
</div>
<button class="btn btn-primary" id="submitBtn" onclick="submitBatch()">全部上傳</button>
<div id="results" class="mt-3"></div>
</div>
</div>
<script>
let selectedFiles = [];
function parseFilename(fname) {
const stem = fname.replace(/\.(xls|xlsx)$/i, '');
const tokens = stem.split(/[_\-. ]+/).filter(x => x);
function isDate(t) {
if (!/^\d{8}$/.test(t)) return false;
const y = +t.slice(0,4), m = +t.slice(4,6), d = +t.slice(6,8);
return y >= 1900 && y <= 2099 && m >= 1 && m <= 12 && d >= 1 && d <= 31;
}
function isDesc(t) {
return /^(PRA\d*|Class\d*|LS\w*|SA\d*|I|II|III)$/i.test(t);
}
function isChartNo(t) {
return /^\d{6,8}$/.test(t) && !isDate(t);
}
let chart = '', name = '';
for (const t of tokens) {
if (isDate(t) || isDesc(t)) continue;
if (isChartNo(t) && !chart) { chart = t; continue; }
if (!/^\d+$/.test(t) && !name) { name = t; }
}
return { name, chart };
}
function onPick(input) {
selectedFiles = Array.from(input.files);
if (!selectedFiles.length) return;
const tbody = document.getElementById('previewBody');
tbody.innerHTML = '';
selectedFiles.forEach((f, i) => {
const p = parseFilename(f.name);
const tr = document.createElement('tr');
tr.dataset.idx = i;
const missing = !p.chart || !p.name;
tr.innerHTML = `
<td class="small">${f.name}</td>
<td><input type="text" class="form-control form-control-sm" data-field="chart" value="${p.chart}"></td>
<td><input type="text" class="form-control form-control-sm" data-field="name" value="${p.name}"></td>
<td class="${missing ? 'status-err' : 'status-ok'}">${missing ? '缺欄位' : '待上傳'}</td>
<td><button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRow(this)">刪除</button></td>
`;
tbody.appendChild(tr);
});
document.getElementById('previewWrap').style.display = 'block';
}
function removeRow(btn) {
const tr = btn.closest('tr');
if (tr) tr.remove();
if (!document.querySelectorAll('#previewBody tr').length) {
document.getElementById('previewWrap').style.display = 'none';
}
}
function submitBatch() {
const btn = document.getElementById('submitBtn');
const mode = document.querySelector('input[name="dupmode"]:checked').value;
const rows = document.querySelectorAll('#previewBody tr');
btn.disabled = true; btn.textContent = '上傳中...';
const results = document.getElementById('results');
results.innerHTML = '<div class="text-muted small">處理中...</div>';
const fd = new FormData();
fd.append('mode', mode);
rows.forEach(tr => {
const idx = parseInt(tr.dataset.idx);
const chart = tr.querySelector('[data-field="chart"]').value.trim();
const name = tr.querySelector('[data-field="name"]').value.trim();
const f = selectedFiles[idx];
fd.append('file', f); fd.append('chart', chart); fd.append('name', name);
});
fetch('/dsa/batch_upload', { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
btn.disabled = false; btn.textContent = '全部上傳';
let html = '<table class="table table-sm table-bordered"><thead><tr><th>檔名</th><th>結果</th></tr></thead><tbody>';
(d.results || []).forEach(r => {
let cls = 'status-ok', msg = '成功新增';
if (r.status === 'error') { cls = 'status-err'; msg = r.msg || '失敗'; }
else if (r.status === 'duplicate') { cls = 'status-dup'; msg = '重複,已複寫'; }
else if (r.status === 'skipped') { cls = 'status-dup'; msg = '重複,跳過不新增'; }
html += `<tr><td class="small">${r.file}</td><td class="${cls}">${msg}</td></tr>`;
});
html += '</tbody></table>';
html += `<div class="mt-2">成功 ${d.summary.ok}、重複 ${d.summary.dup || 0}、失敗 ${d.summary.err}(共 ${d.summary.total})</div>`;
if (d.summary.ok > 0) html += '<a href="/dsa/history" class="btn btn-sm btn-outline-primary mt-2">查看歷史紀錄</a>';
results.innerHTML = html;
})
.catch(e => {
btn.disabled = false; btn.textContent = '全部上傳';
results.innerHTML = '<div class="alert alert-danger">上傳失敗:' + e + '</div>';
});
}
const dz = document.getElementById('dropZone');
['dragenter', 'dragover'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.add('dragover'); }));
['dragleave', 'drop'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.remove('dragover'); }));
dz.addEventListener('drop', e => {
const dt = new DataTransfer();
Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f));
document.getElementById('fileInput').files = dt.files;
onPick(document.getElementById('fileInput'));
});
</script>
</body>
</html>