pradsa / templates /admin.html
plokmii's picture
Upload templates/admin.html with huggingface_hub
b494b66 verified
Raw
History Blame Contribute Delete
9.78 kB
<!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>
<!-- ============ Storage ============ -->
{% 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>
<!-- Edit Modal -->
<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>