plokmii commited on
Commit
bca3055
·
verified ·
1 Parent(s): 232fce0

Upload templates/batch.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. templates/batch.html +168 -0
templates/batch.html ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>批次新增資料</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body { background: #f5f7fa; }
10
+ .drop-zone { border: 2px dashed #adb5bd; border-radius: 12px; padding: 2rem; text-align: center; cursor: pointer; background: #fff; }
11
+ .drop-zone:hover, .drop-zone.dragover { border-color: #0d6efd; background: #e8f0fe; }
12
+ table.preview td, table.preview th { font-size: .88rem; }
13
+ .status-ok { color: #198754; font-weight: bold; }
14
+ .status-err { color: #dc3545; font-weight: bold; }
15
+ .status-dup { color: #fd7e14; font-weight: bold; }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div class="container py-4" style="max-width: 1100px;">
20
+ <div class="d-flex justify-content-between align-items-center mb-3">
21
+ <h2 class="mb-0">批次新增資料</h2>
22
+ <div>
23
+ <a href="/" class="btn btn-dark btn-sm me-1">首頁</a>
24
+ <a href="/history" class="btn btn-outline-secondary btn-sm me-1">歷史紀錄</a>
25
+ <a href="/new" class="btn btn-outline-primary btn-sm">單筆新增</a>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="alert alert-info">
30
+ <strong>檔名格式</strong>:必須包含<strong>病歷號</strong>與<strong>姓名</strong>,用底線 <code>_</code> 或空格分隔。<br>
31
+ 範例:<code>40091041_林陽_PRA2_20240605.xls</code>、<code>47213763 鄭允勝 PRA1.xls</code>。<br>
32
+ 病歷號為連續數字,姓名為非數字字元。系統會自動從檔名抓取。
33
+ </div>
34
+
35
+ <div class="card mb-3">
36
+ <div class="card-body">
37
+ <div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()">
38
+ <div style="font-size:2rem;">&#128193;</div>
39
+ <div class="fw-bold">點擊或拖曳選取多個 XLS 檔</div>
40
+ <input type="file" id="fileInput" multiple accept=".xls,.xlsx" style="display:none" onchange="onPick(this)">
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <div id="previewWrap" style="display:none;">
46
+ <h5>預覽 <span class="text-muted small">(可手動修正病歷號 / 姓名)</span></h5>
47
+ <table class="table table-bordered table-sm bg-white preview">
48
+ <thead class="table-light">
49
+ <tr><th>檔名</th><th style="width:140px;">病歷號</th><th style="width:140px;">姓名</th><th style="width:100px;">狀態</th></tr>
50
+ </thead>
51
+ <tbody id="previewBody"></tbody>
52
+ </table>
53
+ <div class="mb-3">
54
+ <div class="form-check form-check-inline">
55
+ <input class="form-check-input" type="radio" name="dupmode" id="mode-new" value="new" checked>
56
+ <label class="form-check-label" for="mode-new">遇到重複:新增一筆</label>
57
+ </div>
58
+ <div class="form-check form-check-inline">
59
+ <input class="form-check-input" type="radio" name="dupmode" id="mode-overwrite" value="overwrite">
60
+ <label class="form-check-label" for="mode-overwrite">覆寫</label>
61
+ </div>
62
+ <div class="form-check form-check-inline">
63
+ <input class="form-check-input" type="radio" name="dupmode" id="mode-skip" value="skip">
64
+ <label class="form-check-label" for="mode-skip">跳過</label>
65
+ </div>
66
+ </div>
67
+ <button class="btn btn-primary" id="submitBtn" onclick="submitBatch()">全部上傳</button>
68
+ <div id="results" class="mt-3"></div>
69
+ </div>
70
+ </div>
71
+ <script>
72
+ let selectedFiles = [];
73
+
74
+ function parseFilename(fname) {
75
+ const stem = fname.replace(/\.(xls|xlsx)$/i, '');
76
+ const tokens = stem.split(/[_ ]+/);
77
+ let chart = '', name = '';
78
+ for (const t of tokens) {
79
+ if (!t) continue;
80
+ if (/^\d{5,}$/.test(t) && !chart) {
81
+ chart = t;
82
+ } else if (!/^\d+$/.test(t) && !/^PRA\d*$/i.test(t) && !/^\d{6,}$/.test(t) && !name) {
83
+ name = t;
84
+ }
85
+ }
86
+ return { name, chart };
87
+ }
88
+
89
+ function onPick(input) {
90
+ selectedFiles = Array.from(input.files);
91
+ if (!selectedFiles.length) return;
92
+ const tbody = document.getElementById('previewBody');
93
+ tbody.innerHTML = '';
94
+ selectedFiles.forEach((f, i) => {
95
+ const p = parseFilename(f.name);
96
+ const tr = document.createElement('tr');
97
+ tr.dataset.idx = i;
98
+ const missing = !p.chart || !p.name;
99
+ tr.innerHTML = `
100
+ <td class="small">${f.name}</td>
101
+ <td><input type="text" class="form-control form-control-sm" data-field="chart" value="${p.chart}"></td>
102
+ <td><input type="text" class="form-control form-control-sm" data-field="name" value="${p.name}"></td>
103
+ <td class="${missing ? 'status-err' : 'status-ok'}">${missing ? '缺欄位' : '待上傳'}</td>
104
+ `;
105
+ tbody.appendChild(tr);
106
+ });
107
+ document.getElementById('previewWrap').style.display = 'block';
108
+ }
109
+
110
+ function submitBatch() {
111
+ const btn = document.getElementById('submitBtn');
112
+ const mode = document.querySelector('input[name="dupmode"]:checked').value;
113
+ const rows = document.querySelectorAll('#previewBody tr');
114
+ btn.disabled = true;
115
+ btn.textContent = '上傳中...';
116
+ const results = document.getElementById('results');
117
+ results.innerHTML = '<div class="text-muted small">處理中...</div>';
118
+
119
+ const fd = new FormData();
120
+ fd.append('mode', mode);
121
+ rows.forEach(tr => {
122
+ const idx = parseInt(tr.dataset.idx);
123
+ const chart = tr.querySelector('[data-field="chart"]').value.trim();
124
+ const name = tr.querySelector('[data-field="name"]').value.trim();
125
+ const f = selectedFiles[idx];
126
+ fd.append('file', f);
127
+ fd.append('chart', chart);
128
+ fd.append('name', name);
129
+ });
130
+
131
+ fetch('/batch_upload', { method: 'POST', body: fd })
132
+ .then(r => r.json())
133
+ .then(d => {
134
+ btn.disabled = false;
135
+ btn.textContent = '全部上傳';
136
+ let html = '<table class="table table-sm table-bordered"><thead><tr><th>檔名</th><th>結果</th></tr></thead><tbody>';
137
+ (d.results || []).forEach(r => {
138
+ let cls = 'status-ok', msg = '成功';
139
+ if (r.status === 'error') { cls = 'status-err'; msg = '失敗:' + r.msg; }
140
+ else if (r.status === 'duplicate') { cls = 'status-dup'; msg = '重複,已依模式處理:' + r.msg; }
141
+ else if (r.status === 'skipped') { cls = 'status-dup'; msg = '跳過(重複)'; }
142
+ html += `<tr><td class="small">${r.file}</td><td class="${cls}">${msg}</td></tr>`;
143
+ });
144
+ html += '</tbody></table>';
145
+ html += `<div class="mt-2">成功 ${d.summary.ok}、重複 ${d.summary.dup || 0}、失敗 ${d.summary.err}(共 ${d.summary.total})</div>`;
146
+ if (d.summary.ok > 0) html += '<a href="/history" class="btn btn-sm btn-outline-primary mt-2">查看歷史紀錄</a>';
147
+ results.innerHTML = html;
148
+ })
149
+ .catch(e => {
150
+ btn.disabled = false;
151
+ btn.textContent = '全部上傳';
152
+ results.innerHTML = '<div class="alert alert-danger">上傳失敗:' + e + '</div>';
153
+ });
154
+ }
155
+
156
+ // Drag & drop
157
+ const dz = document.getElementById('dropZone');
158
+ ['dragenter', 'dragover'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.add('dragover'); }));
159
+ ['dragleave', 'drop'].forEach(ev => dz.addEventListener(ev, e => { e.preventDefault(); dz.classList.remove('dragover'); }));
160
+ dz.addEventListener('drop', e => {
161
+ const dt = new DataTransfer();
162
+ Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f));
163
+ document.getElementById('fileInput').files = dt.files;
164
+ onPick(document.getElementById('fileInput'));
165
+ });
166
+ </script>
167
+ </body>
168
+ </html>