新增管理員頁面、Word PDF 預覽、租賃日期欄位、SSL docker-compose
- 新增 /admin.html:上傳/刪除範本,HTTP Basic Auth 保護 - Word 預覽改用 LibreOffice PDF 轉換,帶入表單參數即時顯示 - 新增租賃開始/結束年月日、租期年數佔位符支援 - 預覽 loading 遮罩,修正 hidden 被 CSS display:flex 覆蓋的問題 - 左右欄 UI 重構,右欄固定顯示 Word 預覽 - 新增 docker-compose.yml + nginx SSL reverse proxy - admin 密碼改由 ADMIN_PASSWORD 環境變數設定 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
125
public/admin.js
Normal file
125
public/admin.js
Normal file
@@ -0,0 +1,125 @@
|
||||
let adminPassword = sessionStorage.getItem('adminPassword');
|
||||
|
||||
const loginSection = document.getElementById('loginSection');
|
||||
const adminSection = document.getElementById('adminSection');
|
||||
const loginError = document.getElementById('loginError');
|
||||
const uploadMessage = document.getElementById('uploadMessage');
|
||||
|
||||
if (adminPassword) {
|
||||
checkAuth();
|
||||
} else {
|
||||
showLogin();
|
||||
}
|
||||
|
||||
document.getElementById('loginButton').addEventListener('click', () => {
|
||||
adminPassword = document.getElementById('passwordInput').value;
|
||||
checkAuth();
|
||||
});
|
||||
|
||||
document.getElementById('passwordInput').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') document.getElementById('loginButton').click();
|
||||
});
|
||||
|
||||
document.getElementById('uploadButton').addEventListener('click', async () => {
|
||||
const file = document.getElementById('uploadInput').files[0];
|
||||
uploadMessage.textContent = '';
|
||||
if (!file) { uploadMessage.textContent = '請選擇檔案。'; return; }
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('template', file);
|
||||
|
||||
const res = await fetchAdmin('/api/admin/templates', { method: 'POST', body: formData });
|
||||
const body = await res.json().catch(() => ({}));
|
||||
|
||||
if (res.ok) {
|
||||
uploadMessage.textContent = `已上傳:${body.name}`;
|
||||
document.getElementById('uploadInput').value = '';
|
||||
loadTemplates();
|
||||
} else {
|
||||
uploadMessage.textContent = body.error || '上傳失敗。';
|
||||
}
|
||||
});
|
||||
|
||||
async function checkAuth() {
|
||||
loginError.textContent = '';
|
||||
try {
|
||||
const res = await fetchAdmin('/api/admin/check');
|
||||
if (res.ok) {
|
||||
sessionStorage.setItem('adminPassword', adminPassword);
|
||||
showAdmin();
|
||||
} else {
|
||||
loginError.textContent = '密碼錯誤。';
|
||||
adminPassword = null;
|
||||
sessionStorage.removeItem('adminPassword');
|
||||
showLogin();
|
||||
}
|
||||
} catch {
|
||||
loginError.textContent = '連線失敗。';
|
||||
}
|
||||
}
|
||||
|
||||
function showLogin() {
|
||||
loginSection.hidden = false;
|
||||
adminSection.hidden = true;
|
||||
}
|
||||
|
||||
function showAdmin() {
|
||||
loginSection.hidden = true;
|
||||
adminSection.hidden = false;
|
||||
loadTemplates();
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
const list = document.getElementById('templateList');
|
||||
try {
|
||||
const res = await fetch('/api/templates');
|
||||
const { templates } = await res.json();
|
||||
|
||||
if (templates.length === 0) {
|
||||
list.innerHTML = '<p class="message">尚無範本。</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.replaceChildren(...templates.map((name) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = 'display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border)';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = name;
|
||||
label.style.flex = '1';
|
||||
label.style.fontSize = '0.9rem';
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'secondary-button';
|
||||
btn.textContent = '刪除';
|
||||
btn.style.minHeight = '34px';
|
||||
btn.style.padding = '0 14px';
|
||||
btn.addEventListener('click', () => deleteTemplate(name, row));
|
||||
|
||||
row.append(label, btn);
|
||||
return row;
|
||||
}));
|
||||
} catch {
|
||||
list.innerHTML = '<p class="message">載入失敗。</p>';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(name, row) {
|
||||
if (!confirm(`確定要刪除「${name}」?此動作無法復原。`)) return;
|
||||
const res = await fetchAdmin(`/api/admin/templates/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
row.remove();
|
||||
if (!document.getElementById('templateList').children.length) {
|
||||
document.getElementById('templateList').innerHTML = '<p class="message">尚無範本。</p>';
|
||||
}
|
||||
} else {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
alert(body.error || '刪除失敗。');
|
||||
}
|
||||
}
|
||||
|
||||
function fetchAdmin(url, options = {}) {
|
||||
const headers = new Headers(options.headers);
|
||||
headers.set('Authorization', `Basic ${btoa('admin:' + adminPassword)}`);
|
||||
return fetch(url, { ...options, headers });
|
||||
}
|
||||
Reference in New Issue
Block a user