| <!DOCTYPE html> |
| <html lang="zh-TW"> |
| <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"> |
| </head> |
| <body> |
| <div class="container py-4" style="max-width: 1000px;"> |
| <div class="d-flex justify-content-between align-items-center mb-3"> |
| <h2 class="mb-0">後台管理</h2> |
| <div> |
| <a href="/" class="btn btn-dark btn-sm me-1">首頁</a> |
| <a href="/logout" class="btn btn-outline-secondary btn-sm">登出</a> |
| </div> |
| </div> |
|
|
| <h5>使用者管理</h5> |
| <table class="table table-bordered bg-white" style="font-size:.9rem;"> |
| <thead class="table-light"> |
| <tr> |
| <th>ID</th> |
| <th>姓名</th> |
| <th>Email</th> |
| <th>帳號</th> |
| <th>密碼</th> |
| <th>權限</th> |
| <th>註冊時間</th> |
| <th></th> |
| </tr> |
| </thead> |
| <tbody> |
| {% for u in users %} |
| <tr id="user-{{ u.id }}"> |
| <td>{{ u.id }}</td> |
| <td id="name-{{ u.id }}">{{ u.display_name or '' }}</td> |
| <td id="email-{{ u.id }}">{{ u.username }}</td> |
| <td>{{ u.username }}</td> |
| <td>{{ u.password_plain or '***' }}</td> |
| <td> |
| <select class="form-select form-select-sm" style="width:120px;" id="role-{{ u.id }}"> |
| <option value="user" {{ 'selected' if u.role == 'user' else '' }}>一般使用者</option> |
| <option value="admin" {{ 'selected' if u.role == 'admin' else '' }}>管理員</option> |
| </select> |
| </td> |
| <td>{{ u.created_at or '-' }}</td> |
| <td class="text-nowrap"> |
| <button class="btn btn-sm btn-outline-primary me-1" onclick="editUser({{ u.id }})">Edit</button> |
| {% if u.username != username %} |
| <button class="btn btn-sm btn-outline-danger" onclick="deleteUser({{ u.id }}, '{{ u.username }}')">Del</button> |
| {% endif %} |
| </td> |
| </tr> |
| {% endfor %} |
| </tbody> |
| </table> |
| </div> |
|
|
| |
| {% set pra_uploads = uploads | selectattr('source', 'equalto', 'PRA') | list %} |
| {% set dsa_uploads = uploads | selectattr('source', 'equalto', 'DSA') | list %} |
| {% set pra_size = (pra_uploads | sum(attribute='size_kb') / 1024) | round(2) %} |
| {% set dsa_size = (dsa_uploads | sum(attribute='size_kb') / 1024) | round(2) %} |
| <div class="d-flex justify-content-between align-items-center mt-4 mb-2"> |
| <h5 class="mb-0">檔案儲存空間 |
| <small class="text-muted">總計 {{ storage.count }} 個檔案,{{ storage.size_mb }} MB</small> |
| </h5> |
| <div> |
| <a href="/admin/download_all_uploads" class="btn btn-sm btn-outline-success me-1">下載全部(PRA/DSA 分資料夾)</a> |
| <button class="btn btn-sm btn-outline-danger" onclick="cleanupUploads()">清理舊檔案</button> |
| </div> |
| </div> |
| <div class="row g-3 mb-4"> |
| {% for grp in [ |
| {'label': 'PRA', 'badge': 'bg-primary', 'list': pra_uploads, 'size': pra_size, 'idprefix': 'pra'}, |
| {'label': 'DSA', 'badge': 'bg-danger', 'list': dsa_uploads, 'size': dsa_size, 'idprefix': 'dsa'} |
| ] %} |
| <div class="col-md-6"> |
| <div class="card h-100"> |
| <div class="card-header d-flex justify-content-between align-items-center py-2"> |
| <div> |
| <span class="badge {{ grp.badge }} me-1">{{ grp.label }}</span> |
| <strong>{{ grp.list | length }}</strong> 個檔案,共 <strong>{{ grp.size }} MB</strong> |
| </div> |
| {% if grp.list %} |
| <a href="/admin/download_all_uploads?source={{ grp.label }}" class="btn btn-sm btn-outline-primary">下載 {{ grp.label }} 全部</a> |
| {% endif %} |
| </div> |
| <div class="card-body p-2"> |
| {% if grp.list %} |
| <div style="max-height: 360px; overflow-y: auto;"> |
| <table class="table table-sm table-bordered mb-0" style="font-size:.82rem;"> |
| <thead class="table-light" style="position:sticky; top:0;"> |
| <tr><th>檔名</th><th style="width:80px;">大小</th><th style="width:130px;">上傳時間</th><th style="width:120px;">操作</th></tr> |
| </thead> |
| <tbody> |
| {% for u in grp.list %} |
| <tr id="upload-row-{{ grp.idprefix }}-{{ loop.index }}"> |
| <td class="small">{{ u.name }}</td> |
| <td class="text-end">{{ u.size_kb }} KB</td> |
| <td class="small">{{ u.mtime }}</td> |
| <td class="text-nowrap"> |
| <a href="/download_upload/{{ u.name }}" class="btn btn-sm btn-outline-primary py-0 px-2">下載</a> |
| <button class="btn btn-sm btn-outline-danger py-0 px-2" onclick="deleteUpload('{{ u.name }}', '{{ grp.idprefix }}-{{ loop.index }}')">刪除</button> |
| </td> |
| </tr> |
| {% endfor %} |
| </tbody> |
| </table> |
| </div> |
| {% else %} |
| <div class="text-muted small">無 {{ grp.label }} 上傳檔案</div> |
| {% endif %} |
| </div> |
| </div> |
| </div> |
| {% endfor %} |
| </div> |
| <script> |
| function deleteUpload(name, rowKey) { |
| if (!confirm('確定刪除 ' + name + '?')) return; |
| fetch('/admin/delete_upload', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ filename: name }) |
| }).then(r => r.json()).then(d => { |
| if (d.ok) { |
| const row = document.getElementById('upload-row-' + rowKey); |
| if (row) row.remove(); |
| } else { |
| alert('刪除失敗:' + (d.error || '')); |
| } |
| }); |
| } |
| </script> |
|
|
| |
| <div class="modal fade" id="editModal" tabindex="-1"> |
| <div class="modal-dialog modal-sm"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h6 class="modal-title">編輯使用者</h6> |
| <button type="button" class="btn-close" data-bs-dismiss="modal"></button> |
| </div> |
| <div class="modal-body"> |
| <input type="hidden" id="editId"> |
| <div class="mb-2"> |
| <label class="form-label small">姓名</label> |
| <input type="text" class="form-control form-control-sm" id="editName"> |
| </div> |
| <div class="mb-2"> |
| <label class="form-label small">Email / 帳號</label> |
| <input type="text" class="form-control form-control-sm" id="editEmail"> |
| </div> |
| <div class="mb-2"> |
| <label class="form-label small">新密碼(空白=不修改)</label> |
| <input type="text" class="form-control form-control-sm" id="editPassword" placeholder="不修改請留空"> |
| </div> |
| </div> |
| <div class="modal-footer"> |
| <button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">取消</button> |
| <button type="button" class="btn btn-sm btn-primary" onclick="saveUser()">儲存</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> |
| <script> |
| function cleanupUploads() { |
| const months = prompt('清理幾個月前的檔案?', '6'); |
| if (!months) return; |
| fetch('/admin/cleanup_uploads', { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ months: parseInt(months) }) |
| }) |
| .then(r => r.json()) |
| .then(d => { |
| if (d.ok) { alert('已刪除 ' + d.deleted + ' 個檔案'); location.reload(); } |
| else alert('失敗: ' + (d.error || '')); |
| }); |
| } |
| |
| function deleteUser(id, name) { |
| if (!confirm('確定刪除帳號 ' + name + ' ?')) return; |
| fetch('/admin/delete_user/' + id, { method: 'POST' }) |
| .then(r => r.json()) |
| .then(d => { if (d.ok) document.getElementById('user-' + id).remove(); else alert('刪除失敗'); }); |
| } |
| |
| function editUser(id) { |
| document.getElementById('editId').value = id; |
| document.getElementById('editName').value = document.getElementById('name-' + id).textContent.trim(); |
| document.getElementById('editEmail').value = document.getElementById('email-' + id).textContent.trim(); |
| document.getElementById('editPassword').value = ''; |
| new bootstrap.Modal(document.getElementById('editModal')).show(); |
| } |
| |
| function saveUser() { |
| const id = document.getElementById('editId').value; |
| const role = document.getElementById('role-' + id)?.value || 'user'; |
| fetch('/admin/update_user/' + id, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ |
| display_name: document.getElementById('editName').value.trim(), |
| username: document.getElementById('editEmail').value.trim(), |
| password: document.getElementById('editPassword').value, |
| role: role |
| }) |
| }) |
| .then(r => r.json()) |
| .then(d => { if (d.ok) location.reload(); else alert('更新失敗: ' + (d.error || '')); }); |
| } |
| </script> |
| </body> |
| </html> |
|
|